Documize 5.14 REST API 使用详解

Documize Community 5.14.0 REST API 全链实测:Basic 换 JWT、空间/文档/页面/附件/用户/标签分类 CRUD、全文检索与索引重建、空间与文档权限、HTML 导出,含 ?type=saved、page.id、全量更新、中文检索等排障实录与社区版停更风险分析。

Documize知识库文档管理Golang2026/11/02

Documize 5.14 REST API 使用详解

目标:把 Documize Community 5.14.0 的 /api 接口按「认证 → 空间 → 文档 → 页面 → 附件 → 用户 → 标签分类 → 搜索索引 → 权限 → 导出」的顺序完整走一遍。每个端点都给出可直接执行的 curl 命令与本次环境的真实回显,并沉淀 405 路由、?type=saved、page.id、documentId 校验、中文检索等排障实录。

一、环境信息

项目值
操作系统Ubuntu 24.04 LTS(x86_64)
DocumizeCommunity v5.14.0(build 1758297927)
数据库PostgreSQL 16.15(本机 5432)
服务地址http://127.0.0.1:8139
API 前缀/api
演示管理员admin@quzuo.local / q1w2e3r4
演示普通用户apitester2@quzuo.local / q1w2e3r4
认证方式HTTP Basic 换 JWT,后续 Authorization: Bearer <token>
实测日期2026-11-02

💡 说明:本文所有请求都在同一台 Ubuntu 24.04 服务器上对本机 8139 端口执行,回显逐行照抄。文中出现的 dbk 开头 ID 是本次实测真实生成的对象 ID,读者在自己环境执行时以各接口返回的 ID 为准。

二、前置条件

2.1 服务已启动并可访问

本文不重复安装步骤,但要求 8139 上已有正常模式的 Documize 进程,且数据库已完成 setup。先确认版本接口:

curl -s http://127.0.0.1:8139/api/public/version -w " [HTTP %{http_code}]\n"

实测回显:

5.14.0 [HTTP 200]

若返回为空或连接失败,说明进程未启动或端口不是 8139;安装与首次 setup 见 《Ubuntu 24.04 裸机安装 Documize 5.14 与 PostgreSQL 16 实战》。

2.2 演示账号与工具

本次环境的组织、管理员账号由 setup 向导创建:管理员邮箱 admin@quzuo.local,密码 q1w2e3r4。API 调试只需要 curl 与 python3(用于解析 JSON 与拼接 ID),不需要额外 SDK。

为避免每次请求都重复写长令牌,先在当前 shell 里设置两个变量并保持会话不关闭:

export BASE=http://127.0.0.1:8139
curl -s -u ":admin@quzuo.local:q1w2e3r4" -X POST $BASE/api/public/authenticate -o login.json -w "HTTP %{http_code}\n"
export TOKEN=$(python3 -c "import json;print(json.load(open('login.json'))['token'])")

实测回显:

HTTP 200

令牌长度可用下面的命令确认:

python3 -c "import json;print('TOKEN_LEN',len(json.load(open('login.json'))['token']))"

实测回显:

TOKEN_LEN 243

⚠️ 注意:export 只在当前 shell 有效;换终端或重新登录后要重新设置 TOKEN。令牌的签名密钥由 DOCUMIZESALT 决定,salt 不固定时进程重启会使旧令牌全部失效。

三、API 总览与认证

本章先建立对路由分组的整体认识,再完成一次完整的「Basic 换 JWT → 携带 Bearer 访问私有接口 → 校验令牌」闭环。后续所有业务接口都依赖本章拿到的 TOKEN。

3.1 公共路由与私有路由

Documize 把 Web UI 与 API 放在同一进程同一端口,JSON 接口统一以 /api 开头,源码中分为 AddPublic 与 AddPrivate 两组:

分组是否需要令牌典型路径用途
公共否/api/public/version、/api/public/meta版本、站点元信息
公共否/api/public/authenticateBasic 认证换 JWT
公共否/api/public/validate校验令牌并返回当前用户
公共否/api/public/attachment/{orgID}/{attachmentID}?token=附件下载(需查询参数令牌)
私有是/api/space、/api/documents、/api/users业务 CRUD
私有是/api/search、/api/global/search/reindex检索与索引重建
私有是/api/export、/api/fetch导出与批量读取

💡 说明:公共路由并不等于「可以匿名读写」。附件下载虽然挂在 /api/public 下,但仍要求 token 查询参数;无令牌访问会返回 403(见第七章)。

3.2 版本与站点元信息

公共元信息接口无需令牌,适合做健康检查与部署核对:

curl -s $BASE/api/public/meta -o meta.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('meta.json'));print({k:d[k] for k in ['orgId','title','message','authProvider','version']})"

实测回显:

HTTP 200
{'orgId': 'dbk3ulk0ptfkf0kvebc0', 'title': 'QuzuoLab', 'message': 'Knowledge Base', 'authProvider': 'documize', 'version': '5.14.0'}

返回字段中 orgId 是当前组织的内部 ID,后续权限接口需要它;authProvider 为 documize 表示使用本地账号体系,若接入 LDAP/Keycloak 会变成对应值。

3.3 登录换取 JWT

登录接口使用 HTTP Basic,用户名部分必须是 domain:email:password 三段,domain 可留空(即冒号前为空):

curl -s -u ":admin@quzuo.local:q1w2e3r4" -X POST $BASE/api/public/authenticate -o login.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('login.json'));print('TOKEN_LEN',len(d['token']));print('user',d['user']['email'],'admin',d['user']['admin'])"

实测回显:

HTTP 200
TOKEN_LEN 243
user admin@quzuo.local admin True

响应体同时返回 token 与 user 对象。user.admin=True 表示该账号是组织管理员;普通编辑器账号会是 admin=False(见第八章)。

⚠️ 注意:Basic 串里的第一个冒号用于分隔 domain,邮箱里的 @ 不影响解析,但密码中若出现冒号会被截断。示例密码 q1w2e3r4 不含冒号,可直接使用。

3.4 令牌校验与请求头

拿到令牌后,所有私有接口都要加请求头:

Authorization: Bearer <token>

校验令牌本身是否有效,可调用公共校验接口:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/public/validate -o valid.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('valid.json'));print('valid fields:',list(d))"

实测回显:

HTTP 200
valid fields: ['id', 'created', 'revised', 'firstname', 'lastname', 'email', 'initials', 'active', 'editor', 'admin', 'viewUsers', 'analytics', 'global', 'lastVersion', 'theme', 'locale', 'accounts', 'groups']

返回的是当前用户完整对象,字段清单可直接用来判断账号权限位:editor(可编辑内容)、admin(组织管理)、viewUsers(可查看用户列表)、analytics、global(全局管理员)。

3.5 JWT 载荷与 salt

Documize 的令牌是标准 JWT,签名算法 HS256。把载荷段 base64url 解码即可看到声明:

python3 - <<'EOF'
import json,base64
t=json.load(open('login.json'))['token']
p=t.split('.')[1]
p+='='*(-len(p)%4)
d=json.loads(base64.urlsafe_b64decode(p))
print(json.dumps(d,ensure_ascii=False,separators=(',',':')))
EOF

实测回显:

{"domain":"","exp":1825150428,"iss":"Documize","org":"dbk3ulk0ptfkf0kvebc0","sub":"webapp","user":"dbk3ulk0ptfkf0kvebcg"}

把 exp 换算成日期:

date -u -d @1825150428 '+%Y-%m-%dT%H:%M:%SZ'

实测回显:

2027-11-02T10:13:48Z

签发时间对应的查询日是 2026-11-02,因此令牌有效期约一年。org 与 user 是组织、用户内部 ID,服务端用它做租户隔离;sub=webapp、iss=Documize 是固定值。

💡 技术拓展:JWT 的签名算法不止本文用的 HS256。常见对比如下——

算法密钥类型特点适用场景
HS256/HS384/HS512共享密钥对称签名,签发与验签同一密钥,性能好单服务、可信内网
RS256/RS384/RS512RSA 公私钥私钥签发、公钥验签,可公开验签方多服务、第三方接入
ES256/ES384ECDSA 公私钥密钥更短、签名更快资源受限、移动端
PS256RSA-PSS填充方式更安全高安全要求

本文实际采用 Documize 内置的 HS256:签名密钥就是启动配置里的 salt,所有实例必须共享同一个 salt 且连同一个数据库,否则一个实例签发的令牌无法被另一个实例接受。

3.6 认证与授权失败的三种表现

三类失败要区分清楚,排障时才不会把权限问题误判成登录问题:

场景实测请求返回
未带令牌GET /api/space{"Error":"Unauthorized"} 401
令牌伪造Authorization: Bearer invalid.token.value{"Error":"Unauthorized"} 401
密码错误Basic 密码改为 wrongpass{"Error":"Unauthorized"} 401
已登录但无权限普通用户 GET /api/users{"Error":"Forbidden"} 403

无令牌访问空间列表的实测:

curl -s $BASE/api/space -w "HTTP %{http_code}\n"

实测回显:

{Error: 'Unauthorized'}HTTP 401

错误体是 Go 的 JSON 编码格式,{Error: 'Unauthorized'} 是服务端 response.WriteUnauthorizedError 的输出,不是标准 JSON 双引号格式,脚本解析时不能直接 json.loads。

四、空间 API

空间(Space)是文档的归属单元,也是权限的第一层边界。本章完成空间的新增、查询、改名、删除与负向验证,后续文档都必须挂在某个 spaceId 下。

4.1 列出可见空间

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/space -o spaces.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('spaces.json'));print('count',len(d));[print(x['id'],x['name'],'type',x['spaceType']) for x in d]"

实测回显:

HTTP 200
count 2
dbk3vk40ptfkf0kvebdg Engineering type 2
dbk59a40ptfvtanpkhlg Search Lab type 3

spaceType=2 是私有空间,3 是公开/共享空间;接口只返回当前用户有查看权限的空间。

4.2 创建空间

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/space \
  -d '{"name":"API Lab","icon":"book"}' -o sp.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('sp.json'));print(d['id'],d['name'],'spaceType',d['spaceType'])"
export SP=$(python3 -c "import json;print(json.load(open('sp.json'))['id'])")

实测回显:

HTTP 200
dbk66as0ptfr4u8acbhg API Lab spaceType 2

请求体只需要 name;icon 是可选图标名,labelId 可绑定标签。创建成功后立即把 id 存入 SP,后面的文档创建要用它。

4.3 查询单个空间

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/space/$SP -w "\nHTTP %{http_code}\n"

实测回显:

{"id":"dbk66as0ptfr4u8acbhg","created":"2026-11-02T10:17:15.056197Z","revised":"2026-11-02T10:17:15.056197Z","name":"API Lab","desc":"","orgId":"dbk3ulk0ptfkf0kvebc0","userId":"dbk3ulk0ptfkf0kvebcg","labelId":"","spaceType":2,"countCategory":0,"countContent":0,"icon":"book","lifecycle":1,"likes":""}
HTTP 200

countContent 是该空间下文档数量,lifecycle=1 表示正常在线;orgId 与 userId 用于后续权限记录。

4.4 更新空间

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X PUT $BASE/api/space/$SP \
  -d '{"name":"API Lab v2","icon":"book"}' -w "\nHTTP %{http_code}\n"

实测回显:

{"id":"dbk66as0ptfr4u8acbhg","created":"0001-01-01T00:00:00Z","revised":"0001-01-01T00:00:00Z","name":"API Lab v2","desc":"","orgId":"","userId":"","labelId":"","spaceType":0,"countCategory":0,"countContent":0,"icon":"book","lifecycle":0,"likes":""}
HTTP 200

⚠️ 注意:PUT 返回的是服务端接收到的请求模型,不是数据库里的最终对象,所以 created、orgId、spaceType 等字段出现零值。判断是否改名成功要再发一次 GET /api/space/$SP,不要看 PUT 的回显。

4.5 删除空间

curl -s -H "Authorization: Bearer $TOKEN" -X DELETE $BASE/api/space/$SP -w "HTTP %{http_code}\n"

实测回显:

{}HTTP 200

删除成功返回空对象 {} 与 200。空间内文档会随空间一起不可见,删除前先确认已备份或已迁移文档。

4.6 负向与边界

缺少 name:

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/space -d '{"icon":"book"}' -w "HTTP %{http_code}\n"

实测回显:

{Error: 'Missing data'}HTTP 400

查询不存在的空间 ID:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/space/doesnotexist -w "HTTP %{http_code}\n"

实测回显:

{Error: 'Internal server error'}HTTP 400

⚠️ 注意:不存在的空间返回 400 且文案是 Internal server error,不是 404。编写客户端时不要用「404 判断资源不存在」,应按 400 且响应体包含该文案处理。

五、文档 API

文档(Document)位于空间内,本身不含正文,正文按页面(Page)拆分。本章先解释为什么不能直接 POST /api/documents,再给出正确的模板路由创建方式与读改删。

5.1 创建路径探测

官方路由表里 documents 只有 GET,没有 POST。实测直接 POST 与几个常见猜测路径:

探测请求实测结果结论
POST /api/documents405路由未注册 POST
PUT /api/documents/{新ID}403不允许客户端指定 ID
GET /api/templates/{spaceID}null + 200该路径是「已保存模板列表」,不是文档列表
POST /api/import/folder/{spaceID} 空 body400 Missing data是导入接口,不是新建
GET /api/comment404 page not found社区版无此路由

直接 POST 的原始回显:

HTTP 405

💡 说明:Documize 新建文档走的是模板接口 POST /api/templates/{templateID}/folder/{spaceID}?type=saved,templateID=0 表示空模板。这是源码路由 AddPrivate(rt, "templates/{templateID}/folder/{spaceID}", []string{"POST", "OPTIONS"}, []string{"type", "saved"}, template.Use) 的强制查询参数,漏掉会命中另一个模板路由而报 404。

5.2 通过模板路由创建文档

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: text/plain' \
  -X POST "$BASE/api/templates/0/folder/$SP?type=saved" \
  -d 'API Runbook' -o doc.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('doc.json'));print(d['id'],d['name'],'location',d['location'])"
export DOC=$(python3 -c "import json;print(json.load(open('doc.json'))['id'])")

实测回显:

HTTP 200
dbk66as0ptfr4u8acbig API Runbook location template-0

请求体是纯文本标题,不是 JSON;excerpt 默认填 Description,sequence=99999 表示排在最后。location=template-0 说明来自空模板。

5.3 列出与查询文档

按空间列出:

curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/documents?space=$SP" -o docs.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('docs.json'));print('count',len(d));[print(x['id'],x['name'],'seq',x['sequence']) for x in d]"

实测回显:

HTTP 200
count 1
dbk66as0ptfr4u8acbig API Runbook seq 99999

按文档 ID 查询单个对象:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC -o doc_get.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('doc_get.json'));print('keys',list(d))"

实测回显:

HTTP 200
keys ['id', 'created', 'revised', 'orgId', 'spaceId', 'userId', 'job', 'location', 'name', 'excerpt', 'tags', 'template', 'protection', 'approval', 'lifecycle', 'versioned', 'versionId', 'versionOrder', 'sequence', 'groupId', 'category']

对象里没有 slug 字段;文档 slug 由服务端按标题生成,出现在检索结果中(见第十章)。

5.4 更新文档

更新接口的要求容易被误用:请求体必须是 GET 返回的完整文档对象,只传 name/excerpt/tags 会返回 200 但不生效。本节改用独立文档 Doc Update Lab 演示(空间 dbk67v40ptfr4u8acc2g、文档 dbk67v40ptfr4u8acc3g),下文 $DOC 指这份独立文档,避免污染前面 API Runbook 的演示数据。实测对比:

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X PUT $BASE/api/documents/$DOC \
  -d '{"name":"Doc Update Lab local","excerpt":"partial","tags":"x","lifecycle":1}' \
  -w "PUT partial HTTP %{http_code}\n"
curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC -o doc_p.json -w "GET after partial HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('doc_p.json'));print('after partial:',d['name'],'|',d['excerpt'],'| tags',repr(d['tags']))"

实测回显:

{}PUT partial HTTP 200
GET after partial HTTP 200
after partial: Doc Update Lab | Description | tags ''

正确做法是先 GET、本地改字段、再整体 PUT:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC -o doc.json
python3 -c "
import json
d=json.load(open('doc.json'))
d['name']='Doc Update Lab v2'; d['excerpt']='API 实测文档'; d['tags']='api'
open('doc_put.json','w').write(json.dumps(d,ensure_ascii=False))
"
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X PUT $BASE/api/documents/$DOC --data-binary @doc_put.json -w "PUT full HTTP %{http_code}\n"
curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC -o doc2.json -w "GET after full HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('doc2.json'));print('after full:',d['name'],'|',d['excerpt'],'| tags',repr(d['tags']))"

实测回显:

{}PUT full HTTP 200
GET after full HTTP 200
after full: Doc Update Lab v2 | API 实测文档 | tags 'api'

⚠️ 注意:PUT 只回 {},不会回文档对象。改完必须 GET 复核;这也是排查「接口 200 但内容没变」的第一手段。

5.5 删除文档

curl -s -H "Authorization: Bearer $TOKEN" -X DELETE $BASE/api/documents/$DOC -w "HTTP %{http_code}\n"

实测回显:

{}HTTP 200

删除文档会连带删除页面、修订、附件记录;附件二进制也会按存储策略清理(见第七章)。

六、页面 API

页面是文档正文的最小单位。社区版把页面拆成「Page 元数据 + Meta 原始内容」,新增/更新都需要 NewPage 包装结构,并且有严格的 documentId 与 page.id 校验。本章是全文排障点最密集的一章。

6.1 NewPage 请求结构

页面请求体不是扁平的 Page,而是两层:

{"page":{"orgId":"dbk3ulk0ptfkf0kvebc0","documentId":"dbk66as0ptfr4u8acbig","contentType":"text","pageType":"section","level":1,"sequence":1024,"title":"认证与令牌","body":"","status":0},"meta":{"documentId":"dbk66as0ptfr4u8acbig","rawBody":"第一步:用 Basic 认证换取 JWT 令牌","config":"{}"}}

字段含义:

字段说明注意点
page.documentId所属文档必须与路由里的 {documentID} 完全一致
page.contentType渲染类型text/wysiwyg/markdown/code 等,决定 rawBody 的渲染方式
page.pageType页面类型普通章节用 section
page.level层级1 为顶层,2 为子节
page.sequence排序值数值越大越靠后
page.status发布状态数值枚举,0=published
meta.documentId所属文档同样必须与路由一致
meta.rawBody原始内容由服务端按 contentType 渲染成 page.body
meta.configsection 配置至少传 {}

6.2 新增页面

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/documents/$DOC/pages \
  -d "{\"page\":{\"orgId\":\"dbk3ulk0ptfkf0kvebc0\",\"documentId\":\"$DOC\",\"contentType\":\"text\",\"pageType\":\"section\",\"level\":1,\"sequence\":1024,\"title\":\"认证与令牌\",\"body\":\"\",\"status\":0},\"meta\":{\"documentId\":\"$DOC\",\"rawBody\":\"第一步:用 Basic 认证换取 JWT 令牌\",\"config\":\"{}\"}}" \
  -o page.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('page.json'));print(d['id'],d['title'],'status',d['status'],'revisions',d['revisions'])"
export PAGE=$(python3 -c "import json;print(json.load(open('page.json'))['id'])")

实测回显:

HTTP 200
dbk66as0ptfr4u8acbj0 认证与令牌 status 0 revisions 0

status 回显为 0,说明已发布。即使请求里传了其他数值,只要文档未开启审核保护,服务端也会强制写成 0。

新增一个 wysiwyg 页面验证 HTML 渲染:

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/documents/$DOC/pages \
  -d "{\"page\":{\"orgId\":\"dbk3ulk0ptfkf0kvebc0\",\"documentId\":\"$DOC\",\"contentType\":\"wysiwyg\",\"pageType\":\"section\",\"level\":1,\"sequence\":2048,\"title\":\"空间管理\",\"body\":\"\",\"status\":0},\"meta\":{\"documentId\":\"$DOC\",\"rawBody\":\"<h2>空间管理</h2><p>POST /api/space 创建空间</p>\",\"config\":\"{}\"}}" \
  -o page2.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('page2.json'));print(d['id'],d['title'],'body',repr(d['body'][:60]))"

实测回显:

HTTP 200
dbk66as0ptfr4u8acbjg 空间管理 body '<h2>空间管理</h2><p>POST /api/space 创建空间</p>'

⚠️ 注意:rawBody 是 HTML 片段,服务端渲染后写入 page.body。若把 HTML 塞进 text 类型,页面只会按纯文本展示,标签会被转义。

6.3 列表、单页与 meta

列出文档下所有页面:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/pages -o pages.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('pages.json'));print('count',len(d));[print(x['id'],x['title'],x['contentType'],'level',x['level'],'seq',x['sequence']) for x in d]"

实测回显:

HTTP 200
count 2
dbk66as0ptfr4u8acbj0 认证与令牌 text level 1 seq 1024
dbk66as0ptfr4u8acbjg 空间管理 wysiwyg level 1 seq 2048

读取单页对象与原始内容:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/pages/$PAGE -w "\nHTTP %{http_code}\n"
curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/pages/$PAGE/meta -w "\nHTTP %{http_code}\n"

实测回显:

{"id":"dbk66as0ptfr4u8acbj0","created":"2026-11-02T10:17:15.220617Z","revised":"2026-11-02T10:17:15.220617Z","orgId":"dbk3ulk0ptfkf0kvebc0","documentId":"dbk66as0ptfr4u8acbig","userId":"dbk3ulk0ptfkf0kvebcg","contentType":"text","pageType":"section","blockId":"","level":1,"sequence":1024,"numbering":"","title":"认证与令牌","body":"","revisions":0,"status":0,"relativeId":""}
HTTP 200
{"id":4,"created":"2026-11-02T10:17:15.220617Z","revised":"2026-11-02T10:17:15.220617Z","orgId":"dbk3ulk0ptfkf0kvebc0","userId":"dbk3ulk0ptfkf0kvebcg","documentId":"dbk66as0ptfr4u8acbig","pageId":"dbk66as0ptfr4u8acbj0","rawBody":"第一步:用 Basic 认证换取 JWT 令牌","config":"{}","externalSource":false}
HTTP 200

meta.id 是自增数字主键,page.id 是业务引用 ID,两者不是同一个东西;后续更新页面必须用 page.id。

6.4 更新页面

更新请求同样使用 NewPage,但 page.id 必须等于路由里的页面 ID,且 documentId 必须等于路由里的文档 ID。本节与 6.5 改用独立文档 Page Lab 演示(空间 dbk66lc0ptfr4u8acbmg、文档 dbk66lc0ptfr4u8acbng、页面 dbk66lc0ptfr4u8acbo0),下文 $DOC/$PAGE 指这份独立文档与页面:

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X PUT $BASE/api/documents/$DOC/pages/$PAGE \
  -d "{\"page\":{\"id\":\"$PAGE\",\"orgId\":\"dbk3ulk0ptfkf0kvebc0\",\"documentId\":\"$DOC\",\"contentType\":\"text\",\"pageType\":\"section\",\"level\":1,\"sequence\":1024,\"title\":\"配置说明\",\"body\":\"\",\"status\":0},\"meta\":{\"documentId\":\"$DOC\",\"pageId\":\"$PAGE\",\"rawBody\":\"原始内容:端口默认 8139\",\"config\":\"{}\"}}" \
  -o page_up.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('page_up.json'));print(d['id'],d['title'],'revisions',d['revisions'])"

实测回显:

HTTP 200
dbk66lc0ptfr4u8acbo0 配置说明 revisions 1

revisions 从 0 变成 1,说明本次更新生成了修订。只有 status=0(published)且文档生命周期为 live 时才记修订,草稿状态会跳过。

⚠️ 排障实录:更新页面时漏传 page.id,路由校验不通过,服务端在 400 分支里对一个 nil 错误取 err.Error(),直接触发空指针 panic,客户端收到的是空响应(curl 显示 HTTP 000),服务端日志出现 http: panic serving 127.0.0.1:45950: runtime error: invalid memory address or nil pointer dereference。修正方法:page.id 与路由页面 ID 保持一致,page.documentId/meta.documentId 与路由文档 ID 保持一致。

6.5 修订列表与回滚

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/pages/$PAGE/revisions -o rev.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('rev.json'));print('revisions',len(d));[print(x['id'],x['title'],'email',x['email']) for x in d]"

实测回显:

HTTP 200
revisions 1
dbk66lc0ptfr4u8acbog 配置说明 email admin@quzuo.local

回滚到指定修订:

export REV=$(python3 -c "import json;print(json.load(open('rev.json'))[0]['id'])")
curl -s -H "Authorization: Bearer $TOKEN" -X POST $BASE/api/documents/$DOC/pages/$PAGE/revisions/$REV -o rollback.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('rollback.json'));print(d['id'],d['title'],'revisions',d['revisions'])"

实测回显:

HTTP 200
dbk66lc0ptfr4u8acbo0 配置说明 revisions 1

回滚后用 meta 接口确认内容已恢复:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/pages/$PAGE/meta -w "\nHTTP %{http_code}\n"

实测回显:

{"id":6,"created":"2026-11-02T10:17:57.836626Z","revised":"2026-11-02T10:17:57.960498Z","orgId":"dbk3ulk0ptfkf0kvebc0","userId":"dbk3ulk0ptfkf0kvebcg","documentId":"dbk66lc0ptfr4u8acbng","pageId":"dbk66lc0ptfr4u8acbo0","rawBody":"原始内容:端口默认 8139","config":"{}","externalSource":false}
HTTP 200

rawBody 回到更新前的「原始内容:端口默认 8139」,回滚闭环完成。

6.6 页面层级与排序

层级与排序接口都接收数组,是因为 UI 可以一次拖拽多个页面。本节与 6.7 改用独立文档 Tree Lab 演示(空间 dbk679c0ptfr4u8acbsg、文档 dbk679c0ptfr4u8acbtg),先建三页再调整,下文 $PAGE 指其中的「第二节」页面 dbk679c0ptfr4u8acbug:

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/documents/$DOC/pages/level \
  -d "[{\"pageId\":\"$PAGE\",\"level\":2}]" -w "HTTP %{http_code}\n"
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/documents/$DOC/pages/sequence \
  -d "[{\"pageId\":\"$PAGE\",\"sequence\":1536}]" -w "HTTP %{http_code}\n"

实测回显:

{}HTTP 200
{}HTTP 200

复核页面树的编号变化:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/pages -o pages2.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('pages2.json'));[print(x['title'],'level',x['level'],'seq',x['sequence'],'numbering',x['numbering']) for x in d]"

实测回显:

HTTP 200
第一节 level 1 seq 1024 numbering 1
第二节 level 2 seq 1536 numbering 1.1
第三节 level 1 seq 3072 numbering 2

numbering 由服务端按层级重新计算:level=2 的页面挂在第一节下显示为 1.1,后续顶层自动变成 2。

6.7 删除页面

curl -s -H "Authorization: Bearer $TOKEN" -X DELETE $BASE/api/documents/$DOC/pages/$PAGE -w "HTTP %{http_code}\n"

实测回显:

{}HTTP 200

删除后列表只剩两个页面:

count 2 ['第一节', '第三节']

⚠️ 注意:同路径的 DELETE /api/documents/{documentID}/pages 不带页面 ID 时是批量删除接口,误调用会清空整篇文档的页面,脚本里务必带 {pageID}。

七、附件 API

附件挂在文档上,并可通过 page 查询参数关联到具体页面。上传用 multipart,下载走公共路由但必须带令牌。

7.1 上传附件

echo "documize api test file" > att_test.txt
curl -s -H "Authorization: Bearer $TOKEN" \
  -X POST "$BASE/api/documents/$DOC/attachments?page=$PAGE" \
  -F "attachment=@att_test.txt" -w "HTTP %{http_code}\n"

实测回显:

HTTP 200
body: [{}]

上传成功返回空对象;multipart 字段名必须是 attachment,page 查询参数可留空表示只挂文档不挂页面。

7.2 附件列表

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/attachments -o atts.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('atts.json'));print('count',len(d));[print(x['id'],x['filename'],x['extension'],'pageId',x['pageId']) for x in d]"

实测回显:

HTTP 200
count 1
dbk66as0ptfr4u8acbk0 att_test.txt txt pageId dbk66as0ptfr4u8acbj0

pageId 为空说明上传时没带 page 参数;带 ?page= 时会记录到对应页面。

7.3 公共下载

下载路径是 /api/public/attachment/{orgID}/{attachmentID},令牌放在 token 查询参数里,不是请求头:

export ORG=$(python3 -c "import json;print(json.load(open('sp.json'))['orgId'])")
curl -s "$BASE/api/public/attachment/$ORG/$ATT?token=$TOKEN" -w "HTTP %{http_code}\n"

实测回显:

documize api test file
HTTP 200

不带令牌:

curl -s "$BASE/api/public/attachment/$ORG/$ATT" -w "HTTP %{http_code}\n"

实测回显:

{Error: 'Forbidden'}HTTP 403

⚠️ 注意:下载链接里的 orgID 是组织 ID,不是空间 ID;两个都从 dbk 开头,混淆后会得到 404 或空文件。先用 /api/public/meta 的 orgId 核对。

7.4 删除附件

curl -s -H "Authorization: Bearer $TOKEN" \
  -X DELETE $BASE/api/documents/$DOC/attachments/$ATT -w "HTTP %{http_code}\n"

实测回显:

{}HTTP 200

删除后列表返回 [],说明附件记录与文件引用已清理。

八、用户 API

用户接口分两层权限:组织管理员可以创建、列出、改密;普通编辑器只能读自己,列表与创建都会被 403。

8.1 新建用户

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/users \
  -d '{"firstname":"Api","lastname":"Tester","email":"apitester2@quzuo.local","editor":true}' \
  -o user.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('user.json'));print(d['id'],d['email'],'editor',d['editor'],'admin',d['admin'])"
export NUID=$(python3 -c "import json;print(json.load(open('user.json'))['id'])")

实测回显:

HTTP 200
dbk66as0ptfr4u8acbkg apitester2@quzuo.local editor True admin False

⚠️ 注意:创建用户时请求里的密码字段不生效,服务端会随机生成初始密码。要让新用户能登录,必须再用管理员代设密码接口。

8.2 管理员代设密码

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: text/plain' \
  -X POST $BASE/api/users/$NUID/password -d 'q1w2e3r4' -w "HTTP %{http_code}\n"

实测回显:

{}HTTP 200

请求体是纯文本新密码,不是 JSON;该接口只允许管理员或本人调用。

8.3 新用户登录验证

curl -s -u ":apitester2@quzuo.local:q1w2e3r4" -X POST $BASE/api/public/authenticate -o ulogin.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('ulogin.json'));print(d['user']['id'],d['user']['email'],'admin',d['user']['admin'])"
export UTOKEN=$(python3 -c "import json;print(json.load(open('ulogin.json'))['token'])")

实测回显:

HTTP 200
dbk66as0ptfr4u8acbkg apitester2@quzuo.local admin False

8.4 权限边界

普通用户访问用户列表:

curl -s -H "Authorization: Bearer $UTOKEN" $BASE/api/users -w "HTTP %{http_code}\n"

实测回显:

{Error: 'Forbidden'}HTTP 403

管理员访问同一接口返回 200 与用户数组。GET /api/users/{userID} 对普通用户只允许查询自己的 ID,查他人会返回 400。

九、标签与分类 API

标签(Label)是跨空间的横向聚合,分类(Category)是空间内的二级归类。两者都在创建时容易写错 JSON 键名。

9.1 新建与列出标签

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/label -d '{"name":"API 测试二","color":"#280A42"}' \
  -o label.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('label.json'));print(d['id'],d['name'],d['color'])"
export LABEL=$(python3 -c "import json;print(json.load(open('label.json'))['id'])")

实测回显:

HTTP 200
dbk66as0ptfr4u8acblg API 测试二 #280A42

列出全部标签:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/label -o labels.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('labels.json'));print('count',len(d));[print(x['id'],x['name']) for x in d]"

实测回显:

HTTP 200
count 3
dbk65q40ptfr4u8acbgg API 测试
dbk66as0ptfr4u8acblg API 测试二
dbk58q40ptfvtanpkhj0 运维

9.2 新建与列出分类

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/category -d "{\"spaceId\":\"$SP\",\"category\":\"API 分类\"}" \
  -o cat.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('cat.json'));print(d['id'],d['category'],d['isDefault'])"

实测回显:

HTTP 200
dbk66b40ptfr4u8acbm0 API 分类 False

按空间列出:

curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/category/space/$SP?filter=all" -w "\nHTTP %{http_code}\n"

实测回显:

[{"id":"dbk66b40ptfr4u8acbm0","created":"2026-11-02T10:17:16.067459Z","revised":"2026-11-02T10:17:16.067459Z","orgId":"dbk3ulk0ptfkf0kvebc0","spaceId":"dbk66as0ptfr4u8acbhg","category":"API 分类","isDefault":false}]
HTTP 200

⚠️ 注意:分类名称的 JSON 键是 category,不是 name。写成 {"name":"任意名称"} 不会报错,但会创建一个空名称分类。?filter=all 用于返回全部分类,不带时只返回当前用户可见的分类。

十、搜索与索引重建

Documize 不内置搜索引擎,检索完全依赖数据库全文索引表。写操作会异步把文档/页面写入索引,重建接口用于全量刷新。

10.1 关键词检索

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/search \
  -d '{"keywords":"Runbook","doc":true,"content":true,"tag":true}' \
  -o search.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('search.json'));print('hits',len(d));[print(x['document'],x['documentSlug'],x['itemType'],x['excerpt']) for x in d]"

实测回显:

HTTP 200
hits 1
API Runbook api-runbook doc Description

请求体的四个布尔值分别控制是否检索文档标题、正文内容、标签;slog=true 可跳过搜索日志。返回里 documentSlug 是标题自动生成的 slug,可用于拼接前端路由。

10.2 中文关键词

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/search \
  -d '{"keywords":"部署步骤","doc":true,"content":true,"tag":true}' \
  -w "\nHTTP %{http_code}\n"

实测回显:

[]
HTTP 200

中文连续文本返回空数组,原因是 PostgreSQL 默认分词配置不切分中文。该问题属于数据库 FTS 能力,不是接口 bug;需要中文检索时可评估 zhparser 等扩展并调整 text search config。

10.3 索引状态与重建

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/global/search/status -w "\nHTTP %{http_code}\n"
curl -s -H "Authorization: Bearer $TOKEN" -X POST $BASE/api/global/search/reindex -w "\nHTTP %{http_code}\n"

实测回显:

{"entries":8}
HTTP 200
{}HTTP 200

entries 是当前索引表行数;重建接口只允许全局管理员(global=true)调用,普通用户返回 403:

curl -s -H "Authorization: Bearer $UTOKEN" -X POST $BASE/api/global/search/reindex -w "HTTP %{http_code}\n"

实测回显:

{Error: 'Forbidden'}HTTP 403

十一、权限 API

权限分空间级与文档级。空间权限决定能否看到/管理空间与批量操作文档;文档权限是在空间权限之上的额外收紧或放行(编辑、审批)。

11.1 读取空间权限

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/space/$SP/permissions -o sp_perm.json -w "HTTP %{http_code}\n"
python3 -c "import json;d=json.load(open('sp_perm.json'));print('records',len(d));[print(x['whoId'],x['name'],'view',x['spaceView'],'manage',x['spaceManage']) for x in d]"

实测回显:

HTTP 200
records 2
dbk3ulk0ptfkf0kvebcg Zuo San view True manage True
dbk66as0ptfr4u8acbkg Api Tester view True manage False

单条记录包含 12 个动作位:spaceView、spaceManage、spaceOwner、documentAdd、documentEdit、documentDelete、documentMove、documentCopy、documentTemplate、documentApprove、documentLifecycle、documentVersion。

11.2 写入空间权限

请求体是 SpaceRequestModel:Message 用于通知邮件文案,Permissions 是完整记录数组(每次提交都会覆盖旧集合):

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X PUT $BASE/api/space/$SP/permissions \
  -d "{\"Message\":\"API permission test\",\"Permissions\":[{\"orgId\":\"$ORG\",\"spaceId\":\"$SP\",\"whoId\":\"dbk3ulk0ptfkf0kvebcg\",\"who\":\"user\",\"spaceView\":true,\"spaceManage\":true,\"spaceOwner\":true,\"documentAdd\":true,\"documentEdit\":true,\"documentDelete\":true,\"documentMove\":true,\"documentCopy\":true,\"documentTemplate\":true,\"documentApprove\":true,\"documentLifecycle\":true,\"documentVersion\":true,\"name\":\"Zuo San\"},{\"orgId\":\"$ORG\",\"spaceId\":\"$SP\",\"whoId\":\"$NUID\",\"who\":\"user\",\"spaceView\":true,\"spaceManage\":false,\"spaceOwner\":false,\"documentAdd\":false,\"documentEdit\":false,\"documentDelete\":false,\"documentMove\":false,\"documentCopy\":false,\"documentTemplate\":false,\"documentApprove\":false,\"documentLifecycle\":false,\"documentVersion\":false,\"name\":\"Api Tester\"}]}" \
  -w "HTTP %{http_code}\n"

实测回显:

{}HTTP 200

PUT 只回空对象,必须 GET 复核。上面的复核命令会输出两行记录,证明管理员仍是 owner、普通用户只有查看权。

⚠️ 注意:whoId 必须是真实用户 ID。传空字符串或 "0" 会被服务端解释为「Everyone」并把权限授予全体成员,实测曾因此生成一条 name: Everyone 的记录。

11.3 文档权限

读取:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/permissions -w "\nHTTP %{http_code}\n"

实测回显:

[]
HTTP 200

写入(数组,不是包装对象):

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X PUT $BASE/api/documents/$DOC/permissions \
  -d "[{\"orgId\":\"$ORG\",\"documentId\":\"$DOC\",\"whoId\":\"$NUID\",\"who\":\"user\",\"documentRoleEdit\":true,\"documentRoleApprove\":false}]" \
  -w "HTTP %{http_code}\n"

复核:

curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/documents/$DOC/permissions -w "\nHTTP %{http_code}\n"

实测回显:

[{"orgId":"dbk3ulk0ptfkf0kvebc0","documentId":"dbk66as0ptfr4u8acbig","whoId":"dbk66as0ptfr4u8acbkg","who":"user","documentRoleEdit":true,"documentRoleApprove":false}]
HTTP 200

文档权限只有两个动作位:编辑与审批。空间权限里已有 documentEdit=true 的人,仍可被文档级权限收紧。

11.4 普通用户的权限边界

普通用户尝试修改空间权限:

curl -s -H "Authorization: Bearer $UTOKEN" -H 'Content-Type: application/json' \
  -X PUT $BASE/api/space/$SP/permissions -d '{"Message":"x","Permissions":[]}' -w "HTTP %{http_code}\n"

实测回显:

{Error: 'Forbidden'}HTTP 403

普通用户可以正常读取自己有权限的空间与文档,但所有管理类接口(用户列表、权限写入、索引重建)都会被 403 拦截。

十二、导出 API

导出接口把指定范围内容生成一个自包含 HTML 文件,适合离线归档与对外发布。请求体是 exportSpec:

字段说明取值
spaceId空间 ID导出空间/分类/文档时的上下文空间
filterType导出范围space / category / document
data目标 ID 数组空间模式填空间 ID;文档模式填文档 ID

导出整个空间:

curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST $BASE/api/export \
  -d "{\"spaceId\":\"$SP\",\"filterType\":\"space\",\"data\":[\"$SP\"]}" \
  -o export.html -w "HTTP %{http_code} bytes=%{size_download} type=%{content_type}\n"
python3 -c "
s=open('export.html',encoding='utf-8').read();print(s[:110]);print('contains API Runbook:', 'API Runbook' in s)"

实测回显:

HTTP 200 bytes=428938 type=text/html; charset=utf-8
<!DOCTYPE html><html<head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>Do
contains API Runbook: True

💡 说明:导出文件包含内联 CSS 与目录,单文件即可在浏览器打开。空间没有任何可见文档时,正文里会出现 No documents found,仍返回 200。

十三、端点速查表

下表按本文实测顺序整理,是否实测 均指本次环境真实调用:

方法路径用途实测
GET/api/public/version版本200
GET/api/public/meta站点与组织元信息200
POST/api/public/authenticateBasic 换 JWT200
GET/api/public/validate校验令牌200
GET/api/space可见空间列表200 / 401
POST/api/space创建空间200 / 400
GET/api/space/{spaceID}查询空间200 / 400
PUT/api/space/{spaceID}更新空间200
DELETE/api/space/{spaceID}删除空间200
GET/api/documents?space={spaceID}空间内文档列表200
POST/api/templates/{templateID}/folder/{spaceID}?type=saved创建文档200
GET/api/documents/{documentID}查询文档200
PUT/api/documents/{documentID}更新文档(全量对象)200
DELETE/api/documents/{documentID}删除文档200
GET/api/documents/{documentID}/pages页面列表200
POST/api/documents/{documentID}/pages新增页面200 / 400
GET/api/documents/{documentID}/pages/{pageID}单页200
GET/api/documents/{documentID}/pages/{pageID}/meta页面原始内容200
PUT/api/documents/{documentID}/pages/{pageID}更新页面200
DELETE/api/documents/{documentID}/pages/{pageID}删除页面200
POST/api/documents/{documentID}/pages/level调整层级200
POST/api/documents/{documentID}/pages/sequence调整顺序200
GET/api/documents/{documentID}/pages/{pageID}/revisions修订列表200
POST/api/documents/{documentID}/pages/{pageID}/revisions/{revisionID}回滚200
GET/api/documents/{documentID}/attachments附件列表200
POST/api/documents/{documentID}/attachments?page={pageID}上传附件200
DELETE/api/documents/{documentID}/attachments/{attachmentID}删除附件200
GET/api/public/attachment/{orgID}/{attachmentID}?token=下载附件200 / 403
POST/api/users新建用户200 / 403
GET/api/users用户列表200 / 403
POST/api/users/{userID}/password代设密码200
POST/api/label新建标签200
GET/api/label标签列表200
POST/api/category新建分类200
GET/api/category/space/{spaceID}?filter=all分类列表200
POST/api/search关键词检索200
GET/api/global/search/status索引状态200
POST/api/global/search/reindex重建索引200 / 403
GET/api/space/{spaceID}/permissions空间权限读取200
PUT/api/space/{spaceID}/permissions空间权限写入200 / 403
GET/api/documents/{documentID}/permissions文档权限读取200
PUT/api/documents/{documentID}/permissions文档权限写入200
POST/api/export导出 HTML200

十四、常见错误与排障

14.1 401 与 403 混用

现象: 有的请求返回 401,有的返回 403。

定位: 401 表示「没有有效身份」(未带 token、token 伪造、密码错误、salt 变更导致旧 token 失效);403 表示「身份有效但无权限」(普通用户访问用户列表、非管理员调索引重建、无权限改空间权限)。

解决: 先看响应体:Unauthorized 查认证链路,Forbidden 查账号权限位与空间/文档权限记录。

14.2 POST /api/documents 返回 405

现象: 直接按直觉调用 POST /api/documents 得到 405。

原因: 路由表里 documents 只注册了 GET。

解决: 改用 POST /api/templates/0/folder/{spaceID}?type=saved,请求体为纯文本标题。

14.3 创建文档路由 404

现象: 调 POST /api/templates/0/folder/{spaceID} 不带查询参数,得到 404 page not found。

原因: 该路由要求查询参数 type=saved,不满足时会落到其他模板路由。

解决: URL 必须带 ?type=saved,注意 type 是固定值 saved,不是文档类型。

14.4 新增页面 400

现象: POST /api/documents/{documentID}/pages 返回 {"Error":"Bad Request"}。

原因: page.documentId 或 meta.documentId 与路由 {documentID} 不一致;字段缺失时也会走到同一个 400。

解决: 两处 documentId 都填路由里的文档 ID;orgId 从 /api/public/meta 或空间对象里取。

14.5 更新页面收到空响应(HTTP 000)

现象: PUT /api/documents/{documentID}/pages/{pageID} 没有返回码,curl 打印 HTTP 000,服务端日志出现 http: panic serving 127.0.0.1:45950: invalid memory address or nil pointer dereference。

原因: 请求体缺少 page.id,服务端校验 page.id != pageID 后在错误分支对 nil 错误取 err.Error(),触发 panic。

解决: page.id 必须等于路由里的 {pageID};同时确保 page.documentId/meta.documentId 等于 {documentID}。

14.6 更新文档 200 但内容没变

现象: PUT /api/documents/{id} 返回 200,再 GET 仍是旧值。

原因: 请求体只传了部分字段,服务端按完整文档对象覆盖,未传字段被写成零值后又按原逻辑回写,表现为无变化。

解决: 先 GET 完整对象,在本地修改目标字段后整体 PUT 回去(read-modify-write)。

14.7 分类名称为空

现象: 创建分类成功,但列表里 category 是空字符串。

原因: 请求体用了 name 键,服务端只认 category。

解决: 改为 {"spaceId":"<空间ID>","category":"分类名"}。

14.8 中文检索返回空数组

现象: 英文关键词能命中,中文关键词返回 []。

原因: PostgreSQL 默认全文分词不切分中文。

解决: 评估中文分词扩展并调整 FTS 配置,或在业务侧用英文别名/标签辅助检索。该限制同样记录在安装篇的常见问题中。

14.9 重启后所有令牌失效

现象: 进程重启后所有已登录用户请求都返回 401。

原因: 未固定 DOCUMIZESALT,进程每次启动随机生成盐,旧 JWT 验签失败。

解决: 在 systemd unit 或环境变量里固定 salt,并保证所有实例一致;配置优先级细节见 《Documize 5.14 配置参数详解(TOML/env/flags 三层优先级)》。

14.10 删除顺序导致残留

现象: 删了空间但附件仍占用存储,或删用户前忘了转移文档。

原因: 空间删除不级联清理数据库外部的附件对象;用户删除前应先转移其拥有的空间。

解决: 按「附件 → 页面 → 文档 → 空间 → 用户」的顺序清理,删库前先备份。

十五、社区版维护风险与 API 稳定性

这一节把选型风险落到接口层,避免把「接口还能用」误判成「项目在持续维护」。

风险项事实依据(查询日期 2026-11-02)对 API 使用的影响
版本停更社区版最新稳定版 v5.14.0 发布于 2025-09-19,已超过 13 个月无新稳定版接口行为以 v5.14.0 为准,不会再有官方修复;遇 bug 需自行改源码
仓库活跃度下降最近代码推送 2026-05-18,archived=false,open issues 48社区答复与补丁速度有限,排障以本文与源码为主
功能阉割审批流、版本管理、PDF 导出、审计日志等列在 Community+ 商业版社区版 API 的 approval/lifecycle 字段存在,但完整工作流不可用
接口未版本化路由固定 /api,无 /v1 前缀,无 OpenAPI 契约升级前必须做回归测试,不能假定向后兼容
无商业 SLA社区版 AGPL v3,官方支持面向 Community+生产使用需自建监控、备份与升级验证流程

⚠️ 注意:本文所有接口均为 v5.14.0 实测。社区版停更意味着这些行为会长期保持现状:既是「稳定」也是「不再变好」。若项目要求持续安全补丁与商业支持,应在选型阶段评估 Community+ 或替代产品;完整对比见 《Documize 5.14 介绍与选型指南》第七、八节。

十六、参考链接

Documize 5.14 REST API 使用详解 - Quzuo