Skip to content

文档与版本模式篇

目标:让集成方读懂 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=1versionNo=2createTime / comment / label 各自独立),但 blobId 可能相同(节省存储)。
  • 一条文档可以有「当前版本」(最新版本)与「已发布版本」(被 documents/publish 显式发布的某个历史版本)——见 §四。

二、新版本从哪儿来

来源端点触发场景文档身份判定
POST /v1/versions/uploadmultipart 单传一次完成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;若该版本上传时未带 labelpublishedLabel 为 null——集成方做版本列表 UI 时不要假设「发布版必有 label」。

取消发布(GET /v1/documents/unpublish?docId=...

清空 publishedVersionId,文档退回「无已发布版本」状态。

归档 / 取消归档(/v1/documents/archive / /unarchive

  • archivestateARCHIVED——文档对默认查询(state=ACTIVE 筛选)不可见,但仍可读、可发布、版本仍存在。
  • unarchive 还原回 ACTIVE
  • 归档不删除任何版本字节

删除(GET /v1/documents/delete?docId=...

stateTRASHED——进入回收站,对默认查询不可见。版本字节保留,等回收站策略生效。

回收站(/v1/documents/trash/*

端点用途
POST /v1/documents/trash/getPage分页查已删除文档(含 purgeAt 字段:自动彻底清除时间)
GET /v1/documents/trash/restore?docId=...单条恢复(TRASHEDACTIVE
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 去重历史版本。
  • ⚠️ originalFilenametitle 混淆: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

下一步