外观
文档与版本模式篇
目标:让集成方读懂 Document 与 Version 的关系——版本如何产生(multipart / 分片完成 / 预签名 finalize /
versions/save元数据登记)、如何按文档分页查版本、如何回退到历史版本、版本保留策略(maxVersionCount/retentionDays)由谁声明何时生效,以及版本与生命周期投影(发布 / 归档 / 回收站 / 彻底清除)如何交织。不在本篇范围:上传字节的具体三条链路(multipart 单传、分片三段式、预签名直传)——见 文件上传链路模式篇;签发短时直读 URL(
signed-url)也归该篇;按字段筛选文档 / 容器(见 元数据与 schema 模式篇);ACL 与继承(见 ACL 与继承模式篇);分享(ShareLink / ShareGrant);错误码完整码表(见错误码参考)。
本篇是 领域模型 §Document / Version 的纵深展开。概念页讲「文档→版本→Blob」的对象关系,本篇讲版本如何被创建、查询、回退、保留与清理。
一、Document 与 Version 的关系(用前必读)
- Document 是内容对象,归属于一个容器(
containerId),声明为某个 ContentType(documentTypeId,可选;不传时回退租户根类型)。Document 自身不持有字节,字节在它的版本里。 - Version 是文档的每次内容写入产生的不可变快照——
versionNo递增、contentHash(整文件 SHA-256)固定、blobId指向实际字节。版本一旦建立不可改字节;要变内容必须新建版本。 - 多个版本 → 同一 blob 去重不影响版本独立性:把同一份文件上传到一个文档两次,会得到两条独立版本(
versionNo=1与versionNo=2,createTime/comment/label各自独立),但blobId可能相同(节省存储)。 - 一条文档可以有「当前版本」(最新版本)与「已发布版本」(被
documents/publish显式发布的某个历史版本)——见 §四。
二、新版本从哪儿来
| 来源端点 | 触发场景 | 文档身份判定 |
|---|---|---|
POST /v1/versions/upload | multipart 单传一次完成 | 带 containerId + title → 新建文档第 1 版;带 docId → 已有文档追加新版本 |
POST /v1/versions/completeChunkedUpload | 分片三段式的第 3 段 | 与 init 时声明一致——init 带 containerId+title 即新建,带 docId 即追加 |
POST /v1/versions/finalize-upload | 预签名直传的回调段 | 与 presign 时声明一致 |
POST /v1/versions/save | 仅登记已有 blob 的元数据为新版本(不传字节) | 必须带 docId + blobId + contentHash + sizeBytes + mimeType + originalFilename,平台据此追加版本号 |
前三条端点的上传 / 切片 / 直传机制详见 文件上传链路模式篇;本篇只关注版本侧的契约。响应体形态分两种:
upload/completeChunkedUpload/finalize-upload—— 返回FileUploadResultDTO:
json
{
"code": 0,
"data": {
"docId": "D1000001",
"versionId": "V1000002",
"blobId": "B1000001",
"versionNo": 2,
"contentHash": "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447",
"sizeBytes": 12,
"mimeType": "application/pdf",
"originalFilename": "report-v2.pdf"
}
}versions/save—— 返回VersionResultDTO(与 §三 版本列表 / 详情同形态),额外携带comment/label/createBy/createTime等版本元信息;不含mimeType/originalFilename以外的「上传产物」字段——这些字段由调用方在请求里提供,平台只是登记。
⚠️
versions/save不传字节:用于平台外已经持有合法blobId(如平台先经signed-url取过、或某条版本删除后又想登记回去)想登记为某文档的新版本时使用——平台校验blobId存在 +contentHash与平台侧记录一致,再追加版本号。普通业务上传不要用此端点,走 multipart / 分片 / 预签名三条链路之一。
versions/save 最小请求:
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/save' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{
"docId": "${docId}",
"blobId": "${existingBlobId}",
"contentHash": "${sha256}",
"sizeBytes": 12345,
"mimeType": "application/pdf",
"originalFilename": "report.pdf",
"comment": "登记历史 blob",
"label": "v1.0-archived"
}'三、版本查询与详情
分页查版本(POST /v1/versions/getPage)
按 docId 取该文档的版本链:
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/getPage' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{
"docId": "${docId}",
"pageSize": 20,
"pageNumber": 1,
"orderByColumn": "versionNo",
"isAsc": "desc"
}'响应是 VersionResultDTO[],每项含:
| 字段 | 含义 |
|---|---|
versionId | 版本 ID(环境内稳定,用于回退、下载、签 URL) |
versionNo | 版本号(同 docId 内单调递增,从 1 起) |
blobId | 字节寻址;同 contentHash 的版本共享同一 blobId |
contentHash | 整文件 SHA-256 hex |
sizeBytes | 字节数 |
mimeType | 上传时声明的 MIME |
originalFilename | 上传时声明的文件名(不等同 Document.title) |
comment | 上传时附的版本说明 |
label | 上传时一次性写入的版本标签(创建后不可改) |
createBy / createTime | 创建者业务用户 ID / 时间 |
⚠️
versionNo在同一docId内只增不补洞:即便某历史版本被物理清理(保留策略命中、彻底删除等),versionNo序列也不回收——后续新版本继续从max(versionNo)+1开始。集成方不要假设「列表里看到的 versionNo 集合是连续的」。
单版本详情(GET /v1/versions/get?versionId=...)
直接按 versionId 取详情,字段同 VersionResultDTO。
文档详情(GET /v1/documents/get?docId=...)
DocumentDetailResultDTO 携带与当前版本 / 已发布版本相关的关键字段:
json
{
"code": 0,
"data": {
"docId": "D1000001",
"title": "合同 2026Q2",
"state": "ACTIVE",
"currentVersionId": "V1000002",
"currentVersionNo": 2,
"publishedVersionId": "V1000001",
"publishedVersionNo": 1,
"publishedLabel": "v1.0-signed",
"currentVersion": { /* VersionResultDTO 同形态 */ },
"publishedVersion": { /* VersionResultDTO 同形态 */ },
"sizeBytes": 12345,
"mimeType": "application/pdf",
"originalFilename": "report-v2.pdf",
"indexStatus": "INDEXED",
"metadata": [ /* MetadataValueResultDTO[] */ ]
}
}currentVersion*—— 最新版本(versionNo 最大)。publishedVersion*—— 被documents/publish显式发布的版本;可能与currentVersion不一致(先上传新版本、暂不发布,发布版仍指向上一版)。publishedVersionId=null表示该文档无已发布版本。- 顶层
sizeBytes/mimeType/originalFilename来自当前版本,与currentVersion.*一致;前端如要展示「已发布版本的文件名」应读publishedVersion.originalFilename。
四、版本回退
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/revert' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{
"docId": "${docId}",
"versionId": "${historyVersionId}"
}'- 回退不是修改历史——服务端把
versionId指向的字节作为新的快照,追加一条新版本(versionNo继续 +1),comment自动标注源版本,blobId与原历史版本一致(去重)。 - 回退后
currentVersionId指向新追加的回退版本,不是被指向的历史版本。 - ⚠️ 回退不改
publishedVersionId:要让发布也回到老版本,需在回退后再调documents/publish?docId=...&versionId=<新回退版的 versionId>(或某个仍可用的历史 versionId)。这是设计——「发布」是显式动作。
五、版本保留策略(ContentType 上声明)
ContentType 上有两个版本保留控制(见 ContentTypeDTO):
| 字段 | 含义 | null 语义 |
|---|---|---|
maxVersionCount | 保留最近 N 个版本,更早的会被清理 | null = 不限版本数(不是 0、不是未设置) |
retentionDays | 保留 N 天内的版本,超期清理 | null = 不限天数 |
两者同时存在则取并集生效——任一规则命中就清理。规则在「新建新版本」与「定期巡检」两个时机执行,清理对象是对应版本的存储与版本记录,但当前已发布的版本(publishedVersionId 指向的)受保护、不参与清理。
⚠️ null 不是「未设置」——是「主动不限」的策略语义。完整 null 三态见字段值编码契约 §三。
六、版本与生命周期投影
文档自身有 state 状态机:ACTIVE / ARCHIVED / TRASHED / DELETED。版本随文档状态而生效或锁定。
发布(GET /v1/documents/publish?docId=...&versionId=...)
显式把某个版本钉为「已发布版本」(publishedVersionId)。集成方业务通常会把「发布版」作为对外可读的稳定版本——例如分享 / 下载链路只允许签发已发布版本。
- 发布动作不改
currentVersion——只动publishedVersion*字段。 - ⚠️
publishedLabel来自被发布版本的label;若该版本上传时未带label,publishedLabel为 null——集成方做版本列表 UI 时不要假设「发布版必有 label」。
取消发布(GET /v1/documents/unpublish?docId=...)
清空 publishedVersionId,文档退回「无已发布版本」状态。
归档 / 取消归档(/v1/documents/archive / /unarchive)
archive把state置ARCHIVED——文档对默认查询(state=ACTIVE筛选)不可见,但仍可读、可发布、版本仍存在。unarchive还原回ACTIVE。- 归档不删除任何版本字节。
删除(GET /v1/documents/delete?docId=...)
把 state 置 TRASHED——进入回收站,对默认查询不可见。版本字节保留,等回收站策略生效。
回收站(/v1/documents/trash/*)
| 端点 | 用途 |
|---|---|
POST /v1/documents/trash/getPage | 分页查已删除文档(含 purgeAt 字段:自动彻底清除时间) |
GET /v1/documents/trash/restore?docId=... | 单条恢复(TRASHED → ACTIVE) |
POST /v1/documents/trash/batchRestore | 批量恢复(docIds[],单批 ≤ 100) |
GET /v1/documents/trash/purge?docId=... | 单条彻底清除(删除 Document + 所有版本记录;blob 物理清理由平台异步处理) |
POST /v1/documents/trash/batchPurge | 批量彻底清除 |
purgeAt由租户级trashRetentionDays+ 进入回收站时间算得;trashRetentionDays调整后下次响应即生效(不锁定旧值)。- ⚠️
purge是不可逆的物理清除——清除后所有版本无法恢复,集成方做 UI 时务必二次确认。
state=DELETED
DELETED 是物理清除后短暂出现的状态投影,正常业务流不应主动改为 DELETED——documents/update 显式拒绝传入任何 state 值(要改状态走 archive / delete / restore 等专用端点)。
容器侧也有相同的「归档 / 回收站 / 彻底清除」端点(
/v1/containers/archive//trash/getPage//restore//purge等)——见 容器模型与层级模式篇。
七、按版本下载与签发直读 URL
下载 / 直读 URL 始终按版本 ID 寻址,而非文档 ID——文档下「下载最新」就是「下载 currentVersionId」、「下载已发布」就是「下载 publishedVersionId」,调用方自行选取。
POST /v1/versions/signed-url——签发短时直读 URL(mode=download/mode=preview二态;详见 文件上传链路模式篇 §六)。GET /v1/versions/download?versionId=...&signedTicket=...——直接下载流(需先经签名机制取signedTicket,集成方通常用signed-url替代此路径)。
常见坑
- ⚠️
versionNo与列表项数不一致:保留策略清理过历史版本后,getPage返回的最新versionNo与列表长度不相等。规避:用versionNo表达「这是文档的第几次写入」,用列表项做「现存历史」消费,不要把两者画等号。 - ⚠️ 回退后
publishedVersionId没跟上:业务以为「回退即发布回老版」,但发布是独立动作,回退仅追加新版。规避:业务需要时显式再调documents/publish。 - ⚠️ 同 blob 不同版本误认为「重复无用」:去重让两版
blobId相同,运营误以为「这是个 bug,清一条」。规避:去重是存储优化,版本独立性不变;不要按blobId去重历史版本。 - ⚠️
originalFilename与title混淆:UI 上「文档名」用Document.title、「下载文件名」用Version.originalFilename,二者来源不同、内容可能不一致(上传时 metadata 显式给title或上传后documents/update改过)。规避:按场景取对的字段。 - ⚠️
maxVersionCount=null当成 0:把null解析成「不保留任何版本」,误以为「平台会立刻清空版本链」。规避:null = 不限(保留策略层主动声明的「无上限」),见 字段值编码契约 §null 三态。 - ⚠️
publishedVersionId=null仍签 URL 给外部分享:业务规则是「只允许分享已发布版本」,但开发把currentVersion也签了出去。规避:分享 / 公开链路前先校验publishedVersionId非空。 - ⚠️ 回收站
purge当软删:以为只是更彻底的删除、还能恢复。规避:purge不可恢复,UI 二次确认。 - ⚠️
versions/save当成普通上传端点:以为它和versions/upload二选一。规避:save仅登记已有 blob 元数据;正常上传走 multipart / 分片 / 预签名链路。 - ⚠️
indexStatus=INDEX_FAILED长期不处理:以为索引会自愈、却影响搜索可见性。规避:监控DocumentDetailResultDTO.indexStatus,长期失败的通过技术支持渠道反馈。 - ⚠️
label用作运行时可变标签:把label当版本标签随时改、实际它是「创建一次性写入、之后不可改」。规避:要可变的标记位走 metadata 自定义字段,不要复用label。
下一步
- 上传字节怎么走(multipart / 分片 / 预签名) → 文件上传链路模式篇
- 给文档 / 容器授权 → ACL 与继承模式篇
- 给文档挂业务字段、按字段筛选 → 元数据与 schema 模式篇
- 文档归属的容器层级、跨层移动 → 容器模型与层级模式篇
- V1 端点契约 + Try-it → 接口参考