DMS_API_CONTRACT.md 103 KB

方案计划文档管理系统接口契约

契约版本:1.39 状态:已确认,登录图片验证码基线 外部字段风格:camelCase 数据库字段风格:snake_case

1. 使用规则

本文件是 /api/v1 的唯一人工可读接口契约。执行会话必须完整读取 DMS_FUNCTION_CONTRACT.md 和本文件。

后续建立的 backend/openapi/openapi.yaml 是本文件的机器可读镜像,不得另行发明字段、路径或枚举。两者不一致时不得自行选择,必须提交统领会话裁决。

以下旧路径明确废止,不得实现,也不得由前端调用:

/api/v1/documents/{id}/children
/api/v1/documents/{id}/materials
/api/v1/documents/recent
/api/v1/documents/search
/api/v1/documents/{id}/restore
/api/v1/attachments/{id}/preview
/api/v1/attachments/{id}/download

主案子方案、主案附件、搜索、最近更新和文件读取必须使用本文定义的规范路径。

其中 /api/v1/documents/{id}/restore 永久废止,由以下独立回收站资源替代,不提供重定向、别名或兼容调用:

GET  /api/v1/recycle-bin/documents
POST /api/v1/recycle-bin/documents/{id}/restore

2. 通用约定

2.1 基础路径与认证

Base path: /api/v1
Authorization: Bearer <accessToken>
X-Request-Id: <UUID,可选>

除登录和健康检查外,所有接口需要有效 Token。

一次一码轮换(1.38,安全需求:写操作防重放,仅 Java 版实现,开关 DMS_TOKEN_ROTATION_ENABLED 缺省开启):

  • 读请求(GET/HEAD/OPTIONS):沿用当前授权码,不轮换、不比对码代(REST 读重放无危害,且轮询与写并发时在途读请求不掉线);
  • 写请求(POST/PUT/DELETE/PATCH):携带的授权码必须等于该用户当前会话代(sys_user.session_jti = JWT jti),业务成功(响应 < 400)后服务端轮换出新授权码,经响应头 X-DMS-Access-Token: <newAccessToken> 返回,客户端以之替换本地存储作为下一次请求的授权码——旧码对写操作立即失效;
  • 写请求携带已轮换的旧码返回 401 SESSION_ROTATED("授权码已更换,请重新登录"),处理方式与其他 401 一致(清令牌、引导重新登录);
  • 轮换的新码继承原码剩余有效期与全部身份声明(不重置时长、不改变 authVersion);并发写由服务端 CAS 保证任意响应乱序下客户端拿到的都是最新码,前端无需串行化;
  • session_jti 为 NULL(存量会话、开关关闭期间登录、Python 版签发)时跳过比对与轮换,旧令牌自然过渡到重新登录后纳管;开关关闭期间登录不落会话代。

敏感操作另需二次认证:请求携带 X-DMS-Reauth-Token: <reauthToken>(由 POST /api/v1/auth/reauth 重输登录密码换取,5 分钟有效,见 6.3)。缺失、无效、过期、非本人或 authVersion 不符返回 403 REAUTH_REQUIRED。覆盖接口:

  • PUT /api/v1/users/{id}:仅当 securityLevel、roleCode、roleId、status 实际发生变化时强制(纯改名/调部门不要求);
  • PUT /api/v1/documents/{id}/permissions(借阅授权保存,含授予与撤销);
  • POST /api/v1/documents/{id}/permissions/entries/{entryId}/voucher 与 DELETE 同路径(审批单凭证上传与清除);
  • PUT /api/v1/roles/{id}/permissions(角色权限矩阵保存)。

客户端未传 X-Request-Id 时由后端生成。响应头和 JSON 响应体均返回同一个请求 ID:

X-Request-Id: <requestId>

2.2 标识符与时间

  • 数据库主键使用 BIGINT;
  • JSON 中所有 ID 使用十进制字符串,禁止返回 JavaScript 不安全的大整数;
  • 时间统一存储为 UTC;
  • JSON 时间使用带 Z 的 ISO 8601,例如 2026-07-23T03:00:00.000Z;
  • 客户端显示时转换到本地时区。

2.3 JSON成功响应

{
  "code": "OK",
  "message": "success",
  "data": {},
  "requestId": "8ed3e305-73a8-4af7-8f39-1b86b9a2dad2"
}

创建成功使用 HTTP 201,其他普通成功使用 200。删除成功仍返回上述 JSON,不使用 204。

2.4 分页数据

{
  "items": [],
  "page": 1,
  "pageSize": 20,
  "total": 0,
  "totalPages": 0
}

默认值:

  • page=1
  • pageSize=20
  • 最大 pageSize=100

2.5 错误响应

{
  "code": "DATA_VERSION_CONFLICT",
  "message": "数据已被其他用户修改,请刷新后重试",
  "details": {
    "currentRowVersion": 3
  },
  "requestId": "8ed3e305-73a8-4af7-8f39-1b86b9a2dad2"
}

details 无附加信息时为 null,不得省略。

2.6 HTTP状态

HTTP 含义
200 查询、更新、删除或普通操作成功
201 创建成功
400 参数、结构或业务前置条件格式错误
401 未登录、Token无效或已失效
403 角色、密级或操作权限不足
404 对象不存在或已逻辑删除
409 数据版本、唯一性、引用关系等冲突
413 文件、批量数量或请求总体过大
415 文件格式或预览格式不支持
500 未处理的服务器错误

2.7 并发更新

更新、删除、权限全量保存、挂载和解除挂载请求必须携带目标资源的 rowVersion。版本不一致返回:

HTTP 409
code = DATA_VERSION_CONFLICT

成功修改后返回新的 rowVersion。

2.8 Blob响应

预览和下载成功时不使用 JSON 包装。

PDF预览:

Content-Type: application/pdf
Content-Disposition: inline; filename*=UTF-8''...
X-Request-Id: ...

下载:

Content-Type: 实际MIME类型或application/octet-stream
Content-Disposition: attachment; filename*=UTF-8''...
X-Request-Id: ...

文件接口失败时返回普通 JSON 错误响应。

3. 固定枚举

3.1 角色

USER
ADMIN

3.2 模块

DOCUMENT_BROWSER
BACKEND_MANAGEMENT
AUDIT_LOG

3.3 文档类型

MAIN
SUB_PLAN
ATTACHMENT

3.4 文档状态

DRAFT
PUBLISHED
ARCHIVED

3.5 密级

PUBLIC
INTERNAL
SECRET
CONFIDENTIAL
TOP_SECRET

3.6 可见范围

ALL_AUTHENTICATED
ORGANIZATION
CUSTOM

3.7 附件类型

POLICY
WORK_STANDARD
TABLE
DIAGRAM
OTHER

3.8 权限主体

(1.19 起废止:借阅授权仅面向用户主体,doc_permission.subject_type 列与 ORG/USER 枚举一并下线,接口与数据库均不再存在主体类型维度。)

3.9 可执行操作

VIEW
DOWNLOAD
EDIT
CONFIG_PERMISSION
DELETE
BIND_ATTACHMENT
UNBIND_ATTACHMENT
RESTORE_DOCUMENT

3.10 分类类型

SCENE
STYLE
SITUATION
VERSION
OTHER

3.11 启停状态

用户、组织和分类统一使用:

ENABLED
DISABLED

3.12 审计结果

SUCCESS
FAILURE

3.13 审计动作

LOGIN
LOGOUT
VIEW_DOCUMENT
DOWNLOAD_DOCUMENT
UPLOAD_DOCUMENT
BATCH_IMPORT
EDIT_DOCUMENT
DELETE_DOCUMENT
CREATE_CATEGORY
EDIT_CATEGORY
DELETE_CATEGORY
CHANGE_PERMISSION
BIND_ATTACHMENT
UNBIND_ATTACHMENT

3.14 审计目标

AUTH
CATEGORY
DOCUMENT
ATTACHMENT
PERMISSION
ATTACHMENT_BINDING

4. 公共DTO

4.1 UserSummary

{
  "id": "1",
  "username": "admin",
  "realName": "系统管理员",
  "organizationId": "10",
  "organizationName": "机关",
  "roleCode": "ADMIN",
  "securityLevel": "TOP_SECRET",
  "status": "ENABLED",
  "allowedModules": [
    "DOCUMENT_BROWSER",
    "BACKEND_MANAGEMENT"
  ]
}

模块映射(allowedModules)按“角色的权限矩阵与管理位”纯权限动态计算(契约1.35,与用户类别无关、不查文档存在性):

  • 方案权限矩阵含管理类列(上传/编辑/删除/授权/挂载/解除挂载)任一,或具备分类管理位: 渲染 BACKEND_MANAGEMENT,不再渲染 DOCUMENT_BROWSER
  • 否则矩阵含浏览类列(查看/下载)任一:渲染 DOCUMENT_BROWSER
  • 日志审计管理位:渲染 AUDIT_LOG;系统管理管理位:渲染 SYSTEM_MANAGEMENT; 机构/用户/角色管理位任一:渲染 ORGANIZATION_USER_MANAGEMENT(页签内按钮按权限细分)
  • 矩阵与管理位全空(未分配任何权限):兜底渲染 DOCUMENT_BROWSER
  • 内置 admin:恒为 BACKEND_MANAGEMENT、AUDIT_LOG、ORGANIZATION_USER_MANAGEMENT、SYSTEM_MANAGEMENT

前端必须使用后端返回的 allowedModules,不得根据角色自行补全模块。

4.2 CategoryNode

{
  "id": "100",
  "categoryCode": "SCENE_A",
  "categoryName": "XXXX场景A",
  "categoryType": "SCENE",
  "parentId": null,
  "categoryPath": "/XXXX场景A",
  "sortNo": 10,
  "documentCount": 3,
  "rowVersion": 0,
  "children": []
}

4.3 DocumentSummary

{
  "id": "1001",
  "documentName": "2024年度综合应急预案",
  "summary": "方案内容概述",
  "documentType": "MAIN",
  "status": "PUBLISHED",
  "securityLevel": "SECRET",
  "borrowSummary": "张三、李四",
  "categoryId": "100",
  "categoryName": "XXXX场景A",
  "categoryPath": "/XXXX场景A",
  "parentDocumentId": null,
  "rootDocumentId": "1001",
  "attachmentType": null,
  "fileExtension": "docx",
  "tags": ["应急", "年度"],
  "childCount": 3,
  "attachmentCount": 2,
  "viewCount": 12,
  "downloadCount": 4,
  "createdByName": "系统管理员",
  "createdAt": "2026-07-23T03:00:00.000Z",
  "updatedAt": "2026-07-23T03:00:00.000Z",
  "rowVersion": 2,
  "allowedActions": ["VIEW", "DOWNLOAD", "EDIT"]
}

ATTACHMENT 返回时:

  • categoryId/categoryName/categoryPath/parentDocumentId/rootDocumentId 为 null;
  • securityLevel 固定 PUBLIC;
  • attachmentType 必填。

borrowSummary 为借阅摘要:当前生效(canView=true 且未到期)借阅人姓名以 、 连接,无生效借阅时为 未配置 占位;无查看权限的列表行以 - 占位(借阅名单属敏感信息不下发)。仅作展示,不作为权限判定依据。permissionSummary.borrowerCount 与摘要同口径统计生效借阅人数。

allowedActions 各操作位独立判定(1.36 完全拆分):「查看」不再是其他操作位的前置——矩阵勾选任一操作位(含未勾查看)即独立下发该位,写接口同口径放行;DOWNLOAD 由矩阵下载位或借阅ACL下载位独立命中(不再要求同时可查看)。查看类能力(在线预览、详情描述/正文片段、借阅摘要、正文检索命中)仍要求查看权。

4.4 DocumentDetail

DocumentDetail 包含 DocumentSummary 的全部字段,并增加:

{
  "originalFileName": "2024年度综合应急预案.docx",
  "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "fileSize": 204800,
  "fileHash": "SHA-256十六进制字符串",
  "permissionSummary": {
    "borrowerCount": 3,
    "inheritedFromMainPlan": false
  },
  "contentExtractStatus": "SUCCESS",
  "contentExtractError": null,
  "contentExtractedAt": "2026-08-27T03:00:00.000Z"
}

contentExtractStatus/contentExtractError/contentExtractedAt 仅管理员(ADMIN)详情响应返回,用于"编辑"界面排障与重新解析:

  • contentExtractStatus:SUCCESS/EMPTY/UNSUPPORTED/FAILED 之一;
  • contentExtractError:提取失败的脱敏原因(异常类型+首行信息,已去除绝对路径),非 FAILED 时为 null;
  • contentExtractedAt:最近一次正文提取时间(UTC),未提取过为 null;
  • 非管理员详情响应不包含这三个字段,前端必须按可选字段处理。

4.5 RecycleBinDocumentSummary

{
  "id": "1001",
  "documentName": "2024年度综合应急预案",
  "documentType": "MAIN",
  "originalFileName": "2024年度综合应急预案.docx",
  "categoryId": "100",
  "categoryName": "XXXX场景A",
  "parentDocumentId": null,
  "securityLevel": "SECRET",
  "documentStatus": "PUBLISHED",
  "deletedAt": "2026-07-26T03:00:00.000Z",
  "updatedBy": "1",
  "updatedByName": "系统管理员",
  "deletedBy": {
    "userId": "1",
    "username": "admin",
    "realName": "系统管理员",
    "organizationName": "机关"
  },
  "rowVersion": 3
}

categoryId、categoryName、parentDocumentId、updatedBy、updatedByName和deletedBy允许为 null。deletedBy存在时,其四个字段也分别允许为 null。

删除人取与目标ID、目标类型匹配的最近一次成功 DELETE_DOCUMENT 审计快照:

  • MAIN和SUB_PLAN匹配 targetType=DOCUMENT;
  • ATTACHMENT匹配 targetType=ATTACHMENT;
  • 查询必须使用批量关联或窗口函数,不得逐条查询;
  • 没有可靠审计时返回 null;
  • 不得使用 updatedBy 或 updatedByName 冒充删除人。

4.6 RestoreDocumentResult

{
  "document": {},
  "restoredPermissionCount": 2,
  "skippedPermissionCount": 1,
  "restoredBindingCount": 3,
  "skippedBindingCount": 1
}

document使用 DocumentDetail 字段,但恢复响应必须通过纯序列化生成,不得增加 viewCount、写入 VIEW_DOCUMENT 审计或触发文档详情查询的查看副作用。

5. 健康检查

GET /api/v1/health

无需认证。

{
  "code": "OK",
  "message": "success",
  "data": {
    "service": "dms",
    "status": "UP"
  },
  "requestId": "uuid"
}

健康检查不得泄露数据库密码、Token密钥和磁盘绝对路径。

6. 认证接口

6.1 POST /api/v1/auth/login

无需认证。

请求(验证码开启时,见 6.5 与环境变量 DMS_CAPTCHA_ENABLED):

{
  "username": "admin",
  "password": "password",
  "keepSignedIn": true,
  "captchaId": "uuid",
  "captchaCode": "A3BK"
}

验证码关闭时请求体保持三键(username/password/keepSignedIn,不得携带验证码字段);开启时必须且只能包含上述五键。

成功:

{
  "code": "OK",
  "message": "success",
  "data": {
    "accessToken": "jwt",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "tokenRotation": true,
    "idleLogoutMinutes": 5,
    "user": {}
  },
  "requestId": "uuid"
}

user 使用 UserSummary。用户名或密码错误统一返回 401 INVALID_CREDENTIALS,不得泄露用户是否存在。

禁用用户名密码登录(契约1.39,安全管控,仅 Java 版实现,开关 DMS_PASSWORD_LOGIN_DISABLED 缺省关闭):

  • 开关开启时 POST /auth/login 对全部用户(含内置 admin)一律 403 FORBIDDEN("用户名密码登录已被禁用,请使用单点登录"),守卫先于验证码校验与用户查询——不消耗验证码、不泄露账号是否存在;
  • 门户跳转验签与本地 SSO 代理登录(契约1.25)不受影响,是开关开启后仅存的登录入口;
  • GET /auth/captcha 响应 data 必返 passwordLoginDisabled 布尔回显(见6.5),前端据此隐藏账号密码表单、仅显示单点登录提示(隐藏仅为体验,后端守卫为安全兜底)。

验证码校验(契约1.32):

  • 校验先于用户查询与锁定检查(防爆破第一道闸),不写审计、不计入登录失败锁定;
  • 验证码一次性消费:无论本次登录成败,captchaId 校验后即作废,重放返回 400 INVALID_CAPTCHA("验证码错误或已过期");
  • 缺失/错误/过期/未知 captchaId 统一返回 400 INVALID_CAPTCHA,不区分具体原因。

Token有效期:

  • keepSignedIn=false:expiresIn 取环境变量 DMS_JWT_EXPIRES_SECONDS(双版同名,缺省 3600 即 1 小时);
  • keepSignedIn=true:expiresIn=604800(7 天,固定)。

第一阶段不签发刷新令牌。

空闲自动登出(契约1.29新增,门户设密完成响应同):

  • idleLogoutMinutes 取环境变量 DMS_IDLE_LOGOUT_MINUTES(双版同名,缺省 5),0 表示关闭;
  • 前端登录后监测鼠标、键盘、滚动等活动,连续无活动达到该分钟数即调用 logout 自动退出并提示「长时间未操作,已自动退出登录」;
  • 前端将该值随令牌一同按 keepSignedIn 存入 localStorage 或 sessionStorage,会话恢复后沿用。

一次一码轮换(1.38新增,门户设密完成响应同):

  • tokenRotation 为布尔值,取环境变量 DMS_TOKEN_ROTATION_ENABLED(缺省 true,仅 Java 版实现,Python 版恒 false 回显),轮换机制详见 2.1;
  • 登录成功即把新授权码的 jti 落为该用户会话代(开关关闭落 NULL 不纳管);
  • 前端据此字段决定是否启用写操作响应头 X-DMS-Access-Token 换码(关闭时不监听该头)。

6.2 POST /api/v1/auth/logout

请求体为空。成功后递增当前用户 authVersion,使该用户全部旧Token失效。

6.3 POST /api/v1/auth/reauth

敏感操作二次认证(契约1.21新增)。登录态下重输登录密码,换取短时 reauth 令牌。

请求体(严格单键):

{
  "password": "login-password"
}

成功:

{
  "code": "OK",
  "message": "success",
  "data": {
    "reauthToken": "jwt",
    "expiresIn": 300
  },
  "requestId": "uuid"
}

规则:

  • reauth 令牌为 JWT(HS256,同访问令牌密钥),claims sub/type=reauth/authVersion/iat/exp/jti/iss,固定 5 分钟有效,仅可用于 X-DMS-Reauth-Token 请求头,不能作为 Authorization: Bearer 访问令牌(缺 username/roleCode 声明,普通访问校验必然拒绝);
  • 令牌绑定签发人 userId 与 authVersion:authVersion 变化(改角色/停用/登出/改密)后立即失效;
  • 密码错误返回 401 INVALID_CREDENTIALS;不写审计、不计入登录失败锁定;
  • 有效期内多次敏感操作可复用同一令牌,前端应内存缓存并按 expiresIn 提前过期。

6.4 GET /api/v1/users/me

返回当前 UserSummary。

6.5 GET /api/v1/auth/captcha

登录图片验证码签发(契约1.32,安全需求4)。无需认证。

成功(开关开启):

{
  "code": "OK",
  "message": "success",
  "data": {
    "enabled": true,
    "passwordLoginDisabled": false,
    "captchaId": "uuid-hex",
    "image": "data:image/png;base64,..."
  },
  "requestId": "uuid"
}

开关关闭时 data 为 {"enabled": false, "passwordLoginDisabled": <开关当前值>},登录页隐藏验证码输入、登录请求体保持三键。

规则:

  • 开关取环境变量 DMS_CAPTCHA_ENABLED(双版同名,接受 1/true/yes/on,缺省 true 即默认启用);
  • 图片 132×44 PNG(data URL,img 标签 src 直接使用),4 位字符(剔除易混淆 0O1I),忽略大小写校验;
  • 有效期 120 秒,超时未消费自动清理;
  • 一次性消费:登录校验后(无论成败)即作废;
  • 响应带 Cache-Control: no-store,前端不得缓存;验证码获取失败静默隐藏输入框不阻塞登录流程(后端可能未开启);
  • passwordLoginDisabled(契约1.39,仅 Java 版)为禁用用户名密码登录开关 DMS_PASSWORD_LOGIN_DISABLED 的回显,开关开启/关闭两分支均必返,前端开启时隐藏账号密码表单。

6A. 站内通知(契约1.34)

用户站内通知:审批流等待办等需用户处理信息的统一呈现通道。借阅申请审批流(契约1.37,见12.6章)为首个写入方。所有接口需 Bearer 认证,仅操作本人通知。

6A.1 GET /api/v1/notifications

当前用户通知列表(id 倒序,最多返回最近 200 条):

{
  "code": "OK",
  "message": "success",
  "data": {
    "items": [
      {
        "id": "90001",
        "title": "待审批:作战方案A",
        "content": "作战方案A等待您审批",
        "notifyType": "APPROVAL",
        "bizId": "88001",
        "action": "PENDING",
        "status": "UNREAD",
        "createdAt": "2026-09-01T00:00:00.000Z",
        "handledAt": null
      }
    ]
  },
  "requestId": "uuid"
}

字段:

  • notifyType:通知类别,APPROVAL(审批待办),后续按需扩展;
  • bizId:关联业务ID(1.37新增)——APPROVAL 待办指向借阅申请单(消费 GET /api/v1/borrow-requests/{id} 查看审批单),其余通知为 null;
  • action:待办操作——NONE 无需操作 / PENDING 待处理 / AGREE 已同意 / REJECT 已拒绝;
  • status:UNREAD / READ(未读数为红圈徽标数据源);
  • handledAt:同意/拒绝的处理时间,未处理为 null。

6A.2 GET /api/v1/notifications/unread-count

当前用户未读数:data 为 {"count": 2}。前端顶栏用户名红圈 x 与面板「通知」红圈消费本接口。

6A.3 POST /api/v1/notifications/read-all

当前用户全部通知标记已读(打开通知页后清红圈)。响应 data 为 {"updated": 2}(本次标记条数)。幂等:无未读时返回 updated: 0。

6A.4 POST /api/v1/notifications/{id}/handle

处理待办通知。请求体 {"action": "AGREE"} 或 {"action": "REJECT"}。

规则:

  • 仅 action=PENDING 且归属本人的通知可处理,处理后置为 AGREE/REJECT、status 置 READ、记录 handledAt,响应返回处理后的通知;
  • 重复处理或 action=NONE 的通知返回 409 DATA_VERSION_CONFLICT(「该通知已处理或无需处理」);
  • 处理他人通知或不存在返回 404 RESOURCE_NOT_FOUND;action 非法或 id 非正整数返回 400 INVALID_ARGUMENT;
  • 借阅审批联动(1.37):notifyType=APPROVAL 的 PENDING 待办处理时服务端联动借阅申请单审批(同意逐条目落地 doc_permission,详见12.6章),属敏感写操作——请求需携带 X-DMS-Reauth-Token 请求头,缺失返回 403 REAUTH_REQUIRED;申请已被其他审批人处理(任一审批人处理即定)返回 409 DATA_VERSION_CONFLICT(「该借阅申请已被处理」)。

7. 组织和人员

7.1 GET /api/v1/organizations/tree

查询参数:

  • keyword:可选,组织名称或编码;
  • status:可选,按状态过滤;不传时返回全部机构(含停用,契约1.29),停用节点仍保留其祖先链以便展示树形层级。

响应按请求者生效管理范围裁剪(契约1.33):锚点管理员仅返回管理范围子树内的机构(森林形式,根从锚点机构起,范围外机构与其祖先链不出现,keyword/status过滤在裁剪后的集合内进行);全局管理员不受限,返回全部机构。机构管理页、用户管理页与用户编辑「所属机构」下拉均消费本接口。

返回组织树数组,每个节点至少包含:

{
  "id": "10",
  "orgCode": "ORG-10",
  "orgName": "作战部",
  "parentId": null,
  "orgPath": "/作战部",
  "sortNo": 10,
  "status": "ENABLED",
  "children": []
}

7.2 GET /api/v1/users

查询参数:

  • organizationId
  • includeDescendants,默认 false
  • keyword
  • status
  • page
  • pageSize

返回 PageResult<UserSummary>。

8. 分类接口

8.1 GET /api/v1/categories/tree

查询参数:

  • keyword:只搜索分类名称或编码;
  • status:可选。

返回 CategoryNode[]。

documentCount 表示该分类完整有效子树内所有有效主体方案(MAIN)数量,含本分类与全部后代分类,不按当前用户密级/权限过滤,所有登录用户口径一致;计数沿父链穿过停用分类节点累加。

8.2 POST /api/v1/categories

管理员接口。

{
  "categoryCode": "SCENE_A",
  "categoryName": "XXXX场景A",
  "categoryType": "SCENE",
  "parentId": null,
  "sortNo": 10
}

返回新建 CategoryNode,HTTP 201。

8.3 PUT /api/v1/categories/{id}

管理员接口。

{
  "categoryName": "新名称",
  "categoryType": "SCENE",
  "parentId": null,
  "sortNo": 20,
  "rowVersion": 1
}

移动分类不能形成环。成功返回更新后的 CategoryNode。

8.4 DELETE /api/v1/categories/{id}

查询参数:

rowVersion=<integer>

存在有效子分类或有效主案、子方案时返回 409 CATEGORY_IN_USE。

9. 主案与子方案

9.1 GET /api/v1/documents

查询参数:

  • documentType:逗号分隔,可为 MAIN,SUB_PLAN;不用于查询附件;
  • categoryId
  • keyword
  • securityLevel
  • status
  • updatedFrom
  • updatedTo
  • sortBy:仅允许 documentName/createdAt/updatedAt/viewCount/downloadCount
  • sortDirection:asc/desc
  • page
  • pageSize

页签调用规则:

主案列表: documentType=MAIN
全部方案: documentType=MAIN,SUB_PLAN
最近更新: documentType=MAIN,SUB_PLAN&sortBy=updatedAt&sortDirection=desc

关键词搜索也使用本接口,不建立独立 /search 路径。

updatedFrom作用为 updated_at >= updatedFrom,updatedTo作用为 updated_at <= updatedTo;同时提供时为闭区间,且 updatedFrom不得晚于 updatedTo。时间参数必须是包含明确时区的ISO 8601时间,前端统一发送UTC Z格式;空字符串、无时区时间、非法格式或反向区间返回 400 INVALID_ARGUMENT。时间条件在数据库查询和分页之前执行。

返回 PageResult<DocumentSummary>。

9.2 POST /api/v1/documents

管理员接口,multipart/form-data:

  • file:一个文件;
  • metadata:JSON字符串。

主案 metadata:

{
  "documentName": "主案名称",
  "documentType": "MAIN",
  "summary": "概述",
  "categoryId": "100",
  "securityLevel": "SECRET",
  "tags": ["标签"]
}

子方案 metadata 增加:

{
  "documentType": "SUB_PLAN",
  "parentDocumentId": "1001"
}

子方案 parentDocumentId 必须指向有效主案。返回 DocumentDetail,HTTP 201。

名称唯一性与覆盖:

  • 有效 MAIN 的 documentName 全局唯一;
  • 有效 SUB_PLAN 的 documentName 在同一 parentDocumentId 下唯一;
  • 首次上传重名返回 409 DOCUMENT_NAME_CONFLICT,details 包含 existingDocumentId、existingDocumentName、existingRowVersion、documentType、parentDocumentId;
  • 用户确认覆盖后,在原 metadata 中额外同时提交 overwriteDocumentId 和 rowVersion;只提交其中一个返回 400 INVALID_ARGUMENT;
  • 覆盖必须命中同一唯一范围内的当前记录并通过乐观锁,否则返回 409 DATA_VERSION_CONFLICT;
  • 覆盖保留文档ID、主案ACL、附件挂载和父子关系,替换原文件与本次填写的元数据,递增文档版本并写上传审计;事务提交成功后清理旧文件;
  • 批量导入不执行交互式覆盖,重名文件作为该项失败返回 DOCUMENT_NAME_CONFLICT。

9.3 POST /api/v1/documents/batch-import

管理员接口,multipart/form-data:

  • files:重复文件字段;
  • items:JSON数组字符串,与 files 按顺序一一对应。

每个 items[n] 使用单文件 metadata 的结构。数组长度与文件数量不一致返回 400 BATCH_MANIFEST_MISMATCH。

每个文件独立事务,整体 HTTP 200:

{
  "code": "OK",
  "message": "success",
  "data": {
    "total": 2,
    "successCount": 1,
    "failureCount": 1,
    "items": [
      {
        "index": 0,
        "originalFileName": "a.docx",
        "success": true,
        "document": {}
      },
      {
        "index": 1,
        "originalFileName": "b.exe",
        "success": false,
        "errorCode": "UNSUPPORTED_FILE_TYPE",
        "errorMessage": "不支持的文件类型"
      }
    ]
  },
  "requestId": "uuid"
}

9.4 GET /api/v1/documents/{id}

仅用于 MAIN 和 SUB_PLAN,返回 DocumentDetail。

9.5 PUT /api/v1/documents/{id}

只编辑元数据,不替换文件。按文档权限校验:需通过密级检查且具备该文档对应操作权限(管理员恒通过;普通用户由角色矩阵×密级×部门范围与文档授权 OR 叠合决定)。设密主案另需通过对应操作密码锁。(EDIT)

{
  "documentName": "新名称",
  "summary": "新概述",
  "categoryId": "100",
  "securityLevel": "SECRET",
  "status": "PUBLISHED",
  "tags": ["标签1", "标签2"],
  "rowVersion": 2,
  "organizationId": "10"
}

不允许修改 documentType、parentDocumentId、rootDocumentId 和文件身份字段。

organizationId 可选,用于调整文档归属部门(所属单位):缺省沿用现有归属;原样回传现有归属视为未变更; 调整到其他部门仅 ADMIN 可执行(普通用户提交其他部门返回 400 INVALID_ARGUMENT),目标部门必须存在且启用 (否则 404 RESOURCE_NOT_FOUND),子方案与共享附件编辑遵循同一规则。

9.6 DELETE /api/v1/documents/{id}

按文档权限校验:需通过密级检查且具备该文档对应操作权限(管理员恒通过;普通用户由角色矩阵×密级×部门范围与文档授权 OR 叠合决定)。设密主案另需通过对应操作密码锁。(DELETE)

查询参数:

rowVersion=<integer>

主案存在有效子方案时返回 409 MAIN_PLAN_HAS_CHILDREN。成功返回:

{
  "id": "1001",
  "deleted": true
}

9.7 GET /api/v1/main-plans/{id}/sub-plans

查询参数:

  • keyword
  • status
  • updatedFrom
  • updatedTo
  • page
  • pageSize
  • sortBy
  • sortDirection

时间范围作用于直属子方案自身的 updated_at,语义和错误规则与9.1相同。返回 PageResult<DocumentSummary>。id 不是有效主案时返回 404 MAIN_PLAN_NOT_FOUND。

9.8 POST /api/v1/documents/{id}/re-extract

管理员接口(ADMIN),主案/子案/共享附件通用,无请求体。

按当前存储文件重新解析正文并回写提取结果:

  • Office 类型(doc/wps/xls/xlsx/et)先经 LibreOffice 转 PDF 再提取;pdf/docx 直接提取;其余类型恒为 UNSUPPORTED;
  • 覆盖写回 contentText、contentExtractStatus、contentExtractError、contentExtractedAt,并同步重建 search_text(名称+概述+标签+正文);
  • 提取异常不返回 5xx,而是落库为 FAILED 并携带脱敏失败原因(异常类型+首行信息,已去除绝对路径);
  • rowVersion 递增,审计复用 EDIT_DOCUMENT(detail.scope=reExtractContent);
  • 文件在存储中缺失返回 404 FILE_NOT_FOUND。

成功返回 DocumentDetail,HTTP 200。

PDF 提取的换行处理:PDF 复制文本时会在段内软换行处插入 \n。服务端提取时优先按版面结构识别段落——利用 PyMuPDF 的块(block)边界作为段落边界(标题、落款等因字号或间距不同独立成块,不依赖标点猜测),块内合并软换行(相邻行主字号相对差 ≥20% 视为段落边界);当整页为逐行等距同字号的退化网格(如扫描件文本层逐行定位)时回退纯文本启发式——空行、句末标点结尾、条目/章节编号开头视为段落边界保留换行,其余视为段内软换行直接合并(合并处两侧均为 ASCII 字母数字时补一个空格,中文文本直接相连)。页与页之间以空行分隔。

10. 共享附件

10.1 GET /api/v1/attachments

查询参数:

  • keyword
  • attachmentType
  • fileExtension
  • updatedFrom
  • updatedTo
  • sortBy
  • sortDirection
  • page
  • pageSize

返回 PageResult<DocumentSummary>,只包含有效 ATTACHMENT。

该列表对所有登录用户开放且全量可见(支撑全局检索按标题命中全部附件)。普通用户返回的 allowedActions 为空集,且不返回 description 与 contentSnippet;上传、编辑、删除等写操作仍仅 ADMIN,附件内容访问由详情、预览、下载接口按挂载主案权限独立拦截。

10.2 POST /api/v1/attachments

管理员接口,multipart/form-data:

  • file
  • metadata

    {
    "documentName": "附件名称",
    "attachmentType": "POLICY",
    "summary": "说明",
    "tags": ["制度"]
    }
    

后端强制:

documentType=ATTACHMENT
securityLevel=PUBLIC
categoryId=null

返回 DocumentDetail,HTTP 201。

10.3 POST /api/v1/attachments/batch-import

与文档批量导入相同,使用重复 files 和顺序对应的 items。每个 item 使用附件 metadata 结构,每文件独立事务。

10.4 GET /api/v1/attachments/{id}

返回附件 DocumentDetail。

10.5 PUT /api/v1/attachments/{id}

按文档权限校验(EDIT;共享附件仅管理员可维护):

{
  "documentName": "新名称",
  "attachmentType": "WORK_STANDARD",
  "summary": "新说明",
  "tags": ["规范"],
  "rowVersion": 1,
  "organizationId": "10"
}

不得修改附件固定密级、固定可见范围和文件身份。organizationId 可选,语义与9.5一致:缺省沿用现有归属, 调整到其他部门仅 ADMIN 可执行。

10.6 DELETE /api/v1/attachments/{id}

查询参数:

rowVersion=<integer>

存在有效挂载时:

HTTP 409
code = ATTACHMENT_IN_USE
details.mountedPlanCount
details.mainPlans

10.7 GET /api/v1/attachments/{id}/main-plans

返回:

{
  "items": [
    {
      "id": "1001",
      "documentName": "主案名称",
      "categoryName": "XXXX场景A",
      "securityLevel": "SECRET"
    }
  ],
  "total": 1
}

11. 附件挂载

11.1 GET /api/v1/main-plans/{id}/attachments

返回当前主案有效挂载附件,支持:

  • keyword
  • attachmentType
  • updatedFrom
  • updatedTo
  • page
  • pageSize

每项使用附件 DocumentSummary,增加:

{
  "bindingId": "5001",
  "bindingSortNo": 10,
  "mountedPlanCount": 3
}

时间范围作用于附件文档自身的 updated_at,不使用 doc_attachment_binding的创建时间、更新时间或 bindingSortNo;语义和错误规则与9.1相同。

11.2 POST /api/v1/main-plans/{id}/attachments/bind

按文档权限校验:需通过密级检查且具备该文档对应操作权限(管理员恒通过;普通用户由角色矩阵×密级×部门范围与文档授权 OR 叠合决定)。设密主案另需通过对应操作密码锁。(主案 BIND_ATTACHMENT):

{
  "attachmentIds": ["2001", "2002"],
  "mainPlanRowVersion": 3
}

请求内重复ID自动去重;已经挂载的附件不重复创建。

{
  "createdCount": 1,
  "existingCount": 1,
  "attachmentCount": 5,
  "mainPlanRowVersion": 4
}

11.3 DELETE /api/v1/main-plans/{id}/attachments/{attachmentId}

按文档权限校验:需通过密级检查且具备该文档对应操作权限(管理员恒通过;普通用户由角色矩阵×密级×部门范围与文档授权 OR 叠合决定)。设密主案另需通过对应操作密码锁。(主案 UNBIND_ATTACHMENT)

查询参数:

mainPlanRowVersion=<integer>

逻辑删除挂载关系,不删除附件。返回最新附件计数和主案版本。

12. 权限(借阅授权)

1.19 起权限模块收敛为借阅语义:仅面向用户主体的限时授权,操作位只有查看/下载两项,时限必选且最长 7 天;每条授权可选挂一份 PDF 审批单凭证。角色权限矩阵与单文档借阅 ACL 为 OR 叠加;借阅到期即失效(读路径惰性判定 expiresAt > now,无需定时任务)。原可见范围三模式(ALL_AUTHENTICATED/ORGANIZATION/CUSTOM)、组织主体授权与 canEdit/canManagePermission/canDelete 操作位全部下线。

12.1 GET /api/v1/documents/{id}/permissions

按文档权限校验:需通过密级检查且具备该文档对应操作权限(管理员恒通过;普通用户由角色矩阵×密级×部门范围与文档授权 OR 叠合决定)。设密主案另需通过对应操作密码锁。(CONFIG_PERMISSION)

主案返回:

{
  "documentId": "1001",
  "sourceDocumentId": "1001",
  "inherited": false,
  "documentRowVersion": 3,
  "entries": [
    {
      "id": "7001",
      "subjectId": "10",
      "subjectName": "张三",
      "canView": true,
      "canDownload": true,
      "expiresAt": "2026-09-10T03:00:00.000Z",
      "voucherFileName": "借阅审批单.pdf",
      "voucherFileSize": 36752,
      "voucherUploadedAt": "2026-09-03T03:00:00.000Z"
    }
  ]
}
  • entries 按 subjectId 升序返回全部未删除授权(含已到期条目,由前端标记"已到期");
  • expiresAt 为 UTC ISO(Z 结尾)到期时刻;
  • voucherFileName/voucherFileSize/voucherUploadedAt 无凭证时均为 null。

子方案可调用该查询接口,但返回所属主案权限:

  • sourceDocumentId 为主案ID;
  • inherited=true。

共享附件调用返回 400 ATTACHMENT_HAS_NO_ACL。

12.2 PUT /api/v1/documents/{id}/permissions

按文档权限校验:需通过密级检查且具备该文档对应操作权限(管理员恒通过;普通用户由角色矩阵×密级×部门范围与文档授权 OR 叠合决定)。设密主案另需通过对应操作密码锁。(CONFIG_PERMISSION)

敏感操作:请求必须携带有效 X-DMS-Reauth-Token(见 2.1 与 6.3),否则 403 REAUTH_REQUIRED。

只允许对 MAIN 调用。子方案返回 409 SUB_PLAN_PERMISSION_INHERITED。

{
  "documentRowVersion": 3,
  "entries": [
    {
      "subjectId": "10",
      "canView": true,
      "canDownload": true,
      "expiresAt": "2026-09-10T03:00:00.000Z"
    }
  ]
}

规则:

  • 请求是权限全集,不是增量补丁;字段必须且只能包含 documentRowVersion 和 entries;
  • 每条 entry 字段必须且只能包含 subjectId/canView/canDownload/expiresAt,两个动作布尔值均不允许省略;
  • subjectId 必须指向存在、未删除且启用的用户,否则 404 RESOURCE_NOT_FOUND("借阅用户不存在、已删除或已禁用");
  • expiresAt 必须是 Z 结尾的 UTC 时间字符串,必须晚于当前时间,且不超过当前时间 +7 天(服务端含 60 秒容差,覆盖前端分钟粒度拾取舍入),违反返回 400 INVALID_ARGUMENT;
  • 重复主体返回 400 DUPLICATE_PERMISSION_SUBJECT;
  • 保存不触碰审批单凭证:已存在条目保留原凭证;被移除的条目软删除,其凭证文件暂留磁盘(清除凭证走 12.5 独立接口);
  • 后端在一个事务中完成差异更新、版本递增和审计(CHANGE_PERMISSION)。

12.3 POST /api/v1/documents/{id}/permissions/entries/{entryId}/voucher

上传(或替换)指定授权条目的审批单凭证。校验口径与 12.2 相同(仅主案;CONFIG_PERMISSION + 密级 + 密码锁)。敏感操作:需 X-DMS-Reauth-Token(403 REAUTH_REQUIRED)。

  • 请求为 multipart/form-data,必须且只能包含文件字段 file,不得携带任何表单字段,否则 400 INVALID_ARGUMENT("请求必须且只能包含文件 file");
  • 仅允许 PDF(扩展名 + 文件头校验),空文件或非 PDF 返回 415 UNSUPPORTED_FILE_TYPE;
  • 大小受 DMS_MAX_FILE_SIZE 约束,超限返回 413 PAYLOAD_TOO_LARGE("审批单超过允许大小");
  • 授权条目不存在(或不属于该主案)返回 404 RESOURCE_NOT_FOUND("授权条目不存在");
  • 上传为替换语义:新凭证在数据库事务提交成功后提升落位,旧凭证文件随后清理;
  • 成功审计 UPLOAD_PERMISSION_VOUCHER(目标 PERMISSION)。

成功返回:

{
  "documentId": "1001",
  "entryId": "7001",
  "voucherFileName": "借阅审批单.pdf",
  "voucherFileSize": 36752,
  "voucherUploadedAt": "2026-09-03T03:05:00.000Z"
}

12.4 GET /api/v1/documents/{id}/permissions/entries/{entryId}/voucher

下载指定授权条目的审批单凭证。校验口径与 12.3 相同。

  • 成功返回 application/pdf 附件二进制(Content-Disposition: attachment,含 nosniff);
  • 条目未上传凭证返回 404 RESOURCE_NOT_FOUND("该授权未上传审批单");文件缺失返回 404("审批单文件不存在或已被清理")。

12.5 DELETE /api/v1/documents/{id}/permissions/entries/{entryId}/voucher

清除指定授权条目的审批单凭证(授权条目本身保留)。校验口径与 12.3 相同。敏感操作:需 X-DMS-Reauth-Token(403 REAUTH_REQUIRED)。

  • 条目未上传凭证返回 404 RESOURCE_NOT_FOUND("该授权未上传审批单");
  • 凭证文件在数据库事务提交成功后清理;
  • 成功审计 DELETE_PERMISSION_VOUCHER(目标 PERMISSION)。

成功返回:

{
  "documentId": "1001",
  "entryId": "7001"
}

12.6 借阅申请审批流(契约1.37)

借阅授权由手工配置改为申请-审批流:前端隐藏手工「借阅授权」按钮(本接口族与12.1-12.5逻辑保留待切换),主案行改设「借阅」按钮——仅当申请人对该文档既无查看又无下载权限、且密级足够时亮起(与后端同口径判定)。一批用户共用一张申请单(可选附一份PDF审批单)提交后,向所有对该文档有授权权限(CONFIG_PERMISSION)的用户发 APPROVAL+PENDING 待办通知(见6A章),任一审批人在通知中处理即定(先到先得):同意则逐条目落地 doc_permission(各借阅人的查看/下载位与到期时间按申请单),拒绝不落地。

12.6.1 POST /api/v1/documents/{id}/borrow-requests

提交借阅申请,multipart/form-data 两个 part:

  • request(必须):JSON对象字符串,字段固定且仅 entries 与 expiresAt——entries 为条目数组(每条 subjectId/canView/canDownload,不可重复、不可为空,查看与下载至少勾选一项),expiresAt 为UTC时间字符串(Z结尾,须晚于当前时间,最长7天,服务端60秒容差);
  • file(可选):PDF审批单(一批用户共用一张),校验口径与凭证上传一致(application/pdf)。

    {"entries": [{"subjectId": "30001", "canView": true, "canDownload": true}], "expiresAt": "2026-09-23T00:00:00.000Z"}
    

成功返回 {"requestId": "88001", "notified": 2}(notified 为收到待办通知的审批人数)。

规则(敏感操作,需 X-DMS-Reauth-Token,缺失 403 REAUTH_REQUIRED):

  • 仅主案可申请:附件返回 409(附件无权限控制),子案返回 409(子案权限继承主案);
  • 文档不存在返回 404 RESOURCE_NOT_FOUND;申请人密级不足返回 403 SECURITY_LEVEL_FORBIDDEN;
  • 申请人已有查看或下载权限返回 400 INVALID_ARGUMENT("你已拥有该文档的查看或下载权限,无需借阅");
  • 条目用户不存在、已删除或已禁用返回 404;条目用户密级不满足文档密级返回 403 SECURITY_LEVEL_FORBIDDEN;
  • 任一条目用户对该文档已有待审批申请返回 409 BORROW_REQUEST_DUPLICATE("该用户对该文档已有待审批的借阅申请");
  • 无可审批用户(无授权权限用户)返回 400 INVALID_ARGUMENT("暂无可审批该申请的用户,请联系管理员");
  • 审批单文件校验失败沿用既有错误(415 UNSUPPORTED_FILE_TYPE / 413 PAYLOAD_TOO_LARGE);
  • 成功审计 BORROW_REQUEST_CREATE(目标 BORROW_REQUEST,详情含条目、到期时间与通知人数)。

12.6.2 GET /api/v1/borrow-requests/{id}

申请单详情:申请人本人或对该文档有授权权限的用户可见(审批人「查看审批单」)。返回申请单与条目列表:

{
  "requestId": "88001",
  "documentId": "1001",
  "documentName": "作战方案A",
  "requesterId": "30001",
  "requesterName": "张三",
  "entries": [
    {"subjectId": "30001", "subjectName": "张三", "canView": true, "canDownload": true}
  ],
  "expiresAt": "2026-09-23T00:00:00.000Z",
  "voucherFileName": "审批单.pdf",
  "voucherFileSize": 102400,
  "status": "PENDING",
  "handledById": null,
  "handledByName": null,
  "handledAt": null,
  "createdAt": "2026-09-16T00:00:00.000Z"
}
  • status:PENDING 待审批 / APPROVED 已通过 / REJECTED 已拒绝;handledBy*/handledAt 处理后回填(任一审批人处理即定);
  • 申请不存在返回 404 RESOURCE_NOT_FOUND;无权查看(非申请人且无授权权限)返回 403。

12.6.3 GET /api/v1/borrow-requests/{id}/voucher

下载申请审批单(PDF attachment 直出,含 nosniff)。可见性同12.6.2。未上传审批单返回 404 RESOURCE_NOT_FOUND("该申请未上传审批单");文件缺失返回 404("审批单文件不存在或已被清理")。

12.6.4 审批处理(经通知联动)

同意/拒绝不在本接口族——审批人在站内通知待办(6A.4 POST /api/v1/notifications/{id}/handle)中处理:同意逐条目写入 doc_permission(同12.2保存的单条目语义),审计 BORROW_REQUEST_APPROVE;拒绝不落地,审计 BORROW_REQUEST_REJECT。申请已被其他审批人处理返回 409 DATA_VERSION_CONFLICT("该借阅申请已被处理")。

13. 文件读取

以下两个接口适用于 MAIN、SUB_PLAN 和 ATTACHMENT,不再建立附件专用文件路径。

13.1 GET /api/v1/documents/{id}/preview

  • PDF 成功返回 inline Blob,Content-Type 为 application/pdf;
  • DOC、DOCX 由 LibreOffice 离线转换为 PDF 后返回,Content-Type 统一为 application/pdf;
  • XLS、XLSX 返回 415 PREVIEW_UNAVAILABLE;
  • 无查看权限返回 403 DOCUMENT_VIEW_FORBIDDEN;
  • 文件缺失返回 404 FILE_NOT_FOUND;
  • 转换器未启用或 LibreOffice 不可用时返回 503 PREVIEW_CONVERTER_UNAVAILABLE;
  • 转换超时时返回 504 PREVIEW_CONVERSION_TIMEOUT;
  • 转换进程退出码非零、未生成输出或输出非有效 PDF 时返回 502 PREVIEW_CONVERSION_FAILED。

预览接口不新增路径;DOC/DOCX 预览 PDF 为可重新生成的衍生缓存,不属于正式档案;原始 DOC/DOCX 继续通过下载接口返回。

13.2 GET /api/v1/documents/{id}/download

  • 返回原始文件;
  • 主案、子方案需要 DOWNLOAD;
  • 共享附件只检查有效登录状态;
  • 成功后更新下载计数并写审计。

14. 回收站与文档恢复

仅有效 ADMIN 可以访问本章接口。USER返回 403 FORBIDDEN。

本阶段只支持 MAIN、SUB_PLAN和ATTACHMENT的回收站查询与单条恢复。不支持分类、用户或组织恢复,不支持批量恢复、物理删除、自动清理、保留期限、ACL或挂载关系独立恢复,也不移动文件到 recycle 目录。

14.1 GET /api/v1/recycle-bin/documents

查询参数:

  • keyword
  • documentType:MAIN|SUB_PLAN|ATTACHMENT
  • categoryId:字符串ID
  • deletedFrom:带 Z 的ISO 8601 UTC时间
  • deletedTo:带 Z 的ISO 8601 UTC时间
  • page,默认1
  • pageSize,默认20、最大100
  • sortField:仅允许 deletedAt|documentName|documentType|updatedAt
  • sortDirection:asc|desc,默认 desc

固定查询 isDeleted=true 且 deletedAt!=null。删除时间区间采用 [deletedFrom, deletedTo);允许只传一个边界,两者都传时必须满足 deletedFrom < deletedTo。不接受无时区、本地偏移时间或数字时间戳。

keyword只搜索文档名称、概述、searchText和原文件名,使用参数化包含匹配。空字符串按未传处理。

默认排序为 deletedAt desc, id desc;其他排序也使用 id作为同方向次级排序。返回 PageResult<RecycleBinDocumentSummary>,所有ID均为字符串。

14.2 POST /api/v1/recycle-bin/documents/{id}/restore

请求体必须且只能包含:

{
  "rowVersion": 3
}

rowVersion必须为非负整数。成功使用HTTP 200并返回 RestoreDocumentResult。

通用恢复校验:

  1. 记录存在、当前已逻辑删除且类型属于支持范围;
  2. rowVersion与当前删除记录一致;
  3. 文件路径是安全相对路径,解析后仍位于配置存储根目录;
  4. 文件存在且为普通文件,大小和SHA-256与记录一致;
  5. 扩展名、文件头及Office容器结构一致,文件类型仍在允许范围;
  6. 分类、父主案和关系符合对应文档类型要求;
  7. 恢复、关系、计数及成功审计在同一事务提交;
  8. 任一失败整体回滚,文档继续保持逻辑删除;
  9. 不移动、复制或删除原始文件;
  10. 恢复不得强制覆盖关系冲突。

文件检查采用两阶段方案:事务前完成完整哈希和格式校验,保持文件句柄并记录文件状态;事务内锁定数据库记录后、提交前重新检查状态。变化时返回 RESTORE_FILE_CHANGED。数据库事务不能锁定磁盘文件,当前以应用独占管理UUID正式文件为前提。

MAIN恢复采用方案B:

  • 恢复主案本身;
  • 只恢复与本次删除具有相同文档ID、deletedAt和updatedBy的级联删除ACL和挂载关系;
  • 不恢复更早历史删除关系;
  • 权限主体(用户)有效时恢复并刷新主体姓名快照,主体删除或禁用时跳过并计数;
  • 挂载附件有效时恢复,附件删除或不存在时跳过并计数;
  • 已存在同主体有效ACL或同附件有效挂载时返回关系冲突并整体回滚;
  • 借阅语义下恢复的授权保留原 expiresAt,已到期条目由读路径惰性判定失效,不构成恢复失败;
  • 重新统计分类 documentCount、主案 childCount和attachmentCount;
  • allowedActions由现有授权服务动态计算。

SUB_PLAN恢复:

  • parentDocumentId和rootDocumentId必须指向同一有效MAIN;
  • 父主案、根主案及分类必须存在、未删除,分类必须启用;
  • 不恢复或创建独立ACL;
  • 恢复后动态继承主案当前最新权限,不恢复删除时权限;
  • 重新统计父主案 childCount。

ATTACHMENT恢复后强制保持:

documentType=ATTACHMENT
securityLevel=PUBLIC
categoryId=null
parentDocumentId=null
rootDocumentId=null

附件不创建或恢复ACL,不自动恢复历史挂载,mountedPlanCount按当前有效关系统计。若异常存在指向已删除附件的有效挂载,返回关系冲突。

正式允许同类型文档同名、相同原文件名、分类下同名、同一主案下同名子方案、相同文件哈希以及不同文档使用相同哈希但各自保存独立文件路径。恢复冲突只处理有效ACL唯一冲突、有效挂载唯一冲突及主键或结构一致性冲突。

版本规则:

  • 列表返回删除记录当前 rowVersion;
  • 版本不一致返回 409 DATA_VERSION_CONFLICT及 details.currentRowVersion;
  • 文档恢复成功后版本递增一次;
  • 每条恢复ACL和挂载关系分别递增一次;
  • 分类计数变化时分类版本递增一次;
  • SUB_PLAN恢复时父主案版本递增一次;
  • MAIN自身计数更新包含在文档自身一次版本递增中;
  • 跳过关系不修改版本。

成功审计使用 RESTORE_DOCUMENT,包括操作者、组织、目标快照、文档类型、版本前后值、原删除时间、恢复及跳过关系数量、父文档和分类ID、客户端IP、User-Agent和requestId。不得记录绝对路径、文件正文、数据库连接信息或文件哈希。最小范围内恢复失败不写业务审计,只写安全应用日志和统一错误响应。

固定锁顺序:

  1. 根主案或父主案;
  2. 待恢复文档;
  3. 分类;
  4. 删除批次ACL,按ID;
  5. ACL主体组织和用户,按ID;
  6. 关联附件,按ID;
  7. 删除批次挂载关系,按ID。

14.3 B8迁移要求

B8实现必须创建 0002_add_restore_audit_action,不得修改 0001_initial_schema。迁移必须:

  • 扩展 sys_audit_log.action_type CHECK,加入 RESTORE_DOCUMENT;
  • 在 doc_document增加索引 (is_deleted, deleted_at, id);
  • 不修改历史审计;
  • 不新增表;
  • 提供安全upgrade和downgrade;
  • 同步Python枚举和OpenAPI。

15. 审计和统计

15.1 GET /api/v1/audit/logs

查询参数:

  • keyword
  • createdFrom
  • createdTo
  • userId
  • organizationId
  • actionType
  • operationResult
  • targetType
  • targetId
  • page
  • pageSize
  • sortDirection,默认 desc

返回分页日志:

{
  "id": "9001",
  "username": "admin",
  "realName": "系统管理员",
  "organizationName": "机关",
  "actionType": "DOWNLOAD_DOCUMENT",
  "targetType": "DOCUMENT",
  "targetId": "1001",
  "targetName": "主案名称",
  "operationResult": "SUCCESS",
  "failureReason": null,
  "clientIp": "127.0.0.1",
  "requestId": "uuid",
  "createdAt": "2026-07-23T03:00:00.000Z"
}

15.2 GET /api/v1/statistics/documents

{
  "planDocumentCount": 64,
  "mainPlanCount": 21,
  "subPlanCount": 43,
  "attachmentCount": 18,
  "todayViewerCount": 12
}

planDocumentCount = mainPlanCount + subPlanCount,不包含附件。

15.3 GET /api/v1/audit/statistics/trend

参数:createdFrom、createdTo、granularity=DAY|WEEK|MONTH。

返回:

{
  "items": [
    {
      "period": "2026-07-23",
      "operationCount": 25
    }
  ]
}

15.4 GET /api/v1/audit/statistics/actions

参数:createdFrom、createdTo。

返回动作分布:

{
  "items": [
    {
      "actionType": "VIEW_DOCUMENT",
      "count": 20
    }
  ]
}

15.5 GET /api/v1/audit/statistics/users

参数:createdFrom、createdTo、limit,limit 默认10、最大100。

返回活跃用户:

{
  "items": [
    {
      "userId": "1",
      "realName": "系统管理员",
      "organizationName": "机关",
      "operationCount": 15
    }
  ]
}

16. 标准错误码

实现可以增加更细错误码,但不得改变以下语义:

INVALID_ARGUMENT
INVALID_CREDENTIALS
TOKEN_INVALID
TOKEN_EXPIRED
USER_DISABLED
AUTH_VERSION_MISMATCH
SESSION_ROTATED
FORBIDDEN
SECURITY_LEVEL_FORBIDDEN
DOCUMENT_VIEW_FORBIDDEN
DOCUMENT_DOWNLOAD_FORBIDDEN
RESOURCE_NOT_FOUND
FILE_NOT_FOUND
UNSUPPORTED_FILE_TYPE
PREVIEW_UNAVAILABLE
PREVIEW_CONVERTER_UNAVAILABLE
PREVIEW_CONVERSION_TIMEOUT
PREVIEW_CONVERSION_FAILED
PAYLOAD_TOO_LARGE
BATCH_MANIFEST_MISMATCH
DATA_VERSION_CONFLICT
CATEGORY_IN_USE
MAIN_PLAN_HAS_CHILDREN
ATTACHMENT_IN_USE
ATTACHMENT_HAS_NO_ACL
SUB_PLAN_PERMISSION_INHERITED
DUPLICATE_PERMISSION_SUBJECT
INTERNAL_ERROR
DOCUMENT_NOT_DELETED
FILE_INTEGRITY_MISMATCH
FILE_PATH_INVALID
RESTORE_FILE_CHANGED
RESTORE_CATEGORY_INVALID
RESTORE_PARENT_INVALID
RESTORE_RELATION_CONFLICT

恢复错误码定义:

code HTTP message details
DOCUMENT_NOT_DELETED 409 文档当前不在回收站中 documentId
FILE_INTEGRITY_MISMATCH 409 文档文件完整性校验失败 reason=SIZE_MISMATCH\|HASH_MISMATCH\|TYPE_MISMATCH
FILE_PATH_INVALID 500 文档存储路径异常 null
RESTORE_FILE_CHANGED 409 校验期间文件发生变化 null
RESTORE_CATEGORY_INVALID 409 原方案分类无效,无法恢复 categoryId、reason=NOT_FOUND\|DELETED\|DISABLED
RESTORE_PARENT_INVALID 409 原所属主案无效,无法恢复 parentDocumentId、reason=NOT_FOUND\|DELETED\|WRONG_TYPE\|ROOT_MISMATCH
RESTORE_RELATION_CONFLICT 409 历史权限或挂载关系与当前有效关系冲突 permissionConflictCount、bindingConflictCount

(RESTORE_PERMISSION_INVALID 自 1.19 起废止:借阅语义下恢复不再校验"有效权限集合"形态,主体失效一律跳过计数,不再整体拒绝。)

错误响应不得泄露绝对路径、哈希、SQL、数据库连接信息或内部异常堆栈。

17. OpenAPI同步要求

每个业务阶段实现接口时,必须同步更新 OpenAPI,并满足:

  1. 路径、方法和 operationId 唯一;
  2. 请求字段、是否必填和枚举与本文件一致;
  3. 成功和错误响应均有 schema;
  4. 文件接口声明二进制响应和 JSON 错误;
  5. 所有 ID schema 为 type: string、pattern: '^[0-9]+$';
  6. 所有时间 schema 为 type: string、format: date-time;
  7. 不允许用未约束的自由对象代替已定义 DTO;
  8. OpenAPI 校验必须进入自动化测试。
  9. B8实现时将 info.version升级为 1.1.0,同步回收站路径、DTO、错误码、RESTORE_DOCUMENT和迁移后的枚举。
  10. 永久废止的 /api/v1/documents/{id}/restore不得进入OpenAPI。

17A. 统一UI字典

17A.1 查询接口

GET /api/v1/config/ui-dictionaries
  • 认证:有效登录用户,ADMIN、USER均可访问。
  • 请求参数:无。
  • 成功:200,统一响应中的 data 为 UiDictionaries。
  • 失败:无Token或失效Token返回统一 401;启动期配置错误导致应用启动失败,运行期意外错误使用统一 500。
  • 该接口不产生业务审计,字典读取本身不访问数据库,不提供任何配置写接口。

DictionaryOption:

{
  "code": "INTERNAL",
  "label": "内部",
  "sortNo": 20
}

UiDictionaries:

{
  "version": "2026.1",
  "locale": "zh-CN",
  "dictionaries": {
    "securityLevels": [
      {
        "code": "INTERNAL",
        "label": "内部",
        "sortNo": 20
      }
    ]
  }
}

dictionaries必须固定包含以下13个数组:roleCodes、allowedModules、documentTypes、documentStatuses、securityLevels、attachmentTypes、deptScopes、allowedActions、categoryTypes、enabledStatuses、auditResults、auditActions、auditTargets。(1.19 起移除 visibilityTypes 与 subjectTypes。)

每个数组按 sortNo、code稳定排序。外部字段使用 camelCase,sortNo为整数,code和label为字符串。响应不得包含配置路径、加载时间或内部异常;响应体 requestId 与 X-Request-Id保持一致,CORS沿用现有 /api/v1规则。

默认配置位于 backend/dms/resources/ui-dictionaries.zh-CN.json,可通过服务端环境变量 DMS_UI_DICTIONARY_CONFIG_PATH覆盖。配置在启动时严格校验并加载,修改后重启生效,不提供热更新。

18. 前后端实现纪律

前端:

  • 不得自行更名字段或兼容旧路径;
  • 不得把 HTTP 200 等同于业务成功,必须解析 code;
  • 文件接口按 Blob 处理,错误时再解析 JSON;
  • 401 统一清理Token并触发登录失效;
  • 业务按钮使用 allowedActions;
  • 所有 ID 使用字符串。

后端:

  • 不得同时保留两套同义业务路径;
  • 不得把数据库 snake_case 直接泄露到 JSON;
  • 不得信任前端提交的冗余名称、计数、固定附件权限和文件路径;
  • 所有正常查询默认排除逻辑删除数据;
  • 权限、密级、类型和版本必须在服务端校验;
  • requestId、业务异常和审计必须贯穿服务层。

19. 变更控制

执行会话不得直接修改本文件。接口需要变化时,应提交:

  1. 原路径、字段或语义;
  2. 拟修改内容;
  3. 前端影响;
  4. 后端和数据库影响;
  5. 兼容或迁移策略;
  6. 测试影响。

经统领会话确认后,提高契约版本,并同步功能契约、OpenAPI、前端类型和自动化测试。

版本1.1经统领会话批准,增加独立回收站查询和单条文档恢复,旧文档恢复路径继续永久废止。

版本1.2经统领会话批准,增加 GET /api/v1/config/ui-dictionaries 只读接口和严格启动配置规则;既有业务路径、枚举代码及数据库值不变,OpenAPI同步升级为 1.2.0。

版本1.3经统领会话批准,为方案、直属子方案和已挂载附件三个既有查询接口统一增加文档更新时间闭区间筛选;不新增路径、DTO、数据库字段或迁移,OpenAPI同步升级为 1.3.0。Office预览使用1.4或后续版本。

版本1.12经统领会话批准,新增 POST /api/v1/documents/{id}/re-extract 管理员接口;DocumentDetail 增加仅管理员可见的 contentExtractStatus/contentExtractError/contentExtractedAt 可选字段;PDF 正文提取合并段内软换行;数据库新增 doc_document.content_extract_error 列(迁移 0019_content_extract_error),OpenAPI同步升级为 1.12.0。

版本1.13经统领会话批准,新增公开接口 GET /api/v1/config/login-background(登录前无凭证访问):按序探测服务端运行目录下 background.jpg、background.png,命中即返回图片二进制(image/jpeg/image/png,含 nosniff 与 public, max-age=60 缓存头);均不存在时返回 404 FILE_NOT_FOUND 标准错误体,前端回退默认纯色背景。前端登录类界面(账号登录、门户跳转登录、认证状态页)自动探测该接口,有图时拉伸铺满(cover 居中不重复,保留纯色兜底),同一会话只探测一次。无数据库变更,OpenAPI同步升级为 1.13.0。

版本1.14经统领会话批准:①附件类型扩展四值ACCOMPANYING_PLAN(配套方案,UI字典attachmentTypes居首),原SUB_PLAN文档停止新建(创建请求返回400 INVALID_ARGUMENT"配套方案已并入共享资料库,请以附件类型ACCOMPANYING_PLAN上传"),存量子案由迁移0020_sub_plan_to_attachment转为该类型附件(保留密级、置空父/根/分类、父子关系转为挂载关系);②主案allowedActions对ADMIN恒含UNBIND_ATTACHMENT(与BIND对称,移除"存在附件挂载"前置);③附件shape约束放宽(security_level不再强制PUBLIC),附件详情/预览/下载增加密级前置(密级不足返回403 SECURITY_LEVEL_FORBIDDEN,任何角色含ADMIN一致);④共享资料库条目均携带attachmentType四类之一,配套方案Tab的上传/挂载/解绑/排序与其余三类附件同构。OpenAPI同步升级为1.14.0。

版本1.15经统领会话批准,机构维护增强:PUT /api/v1/organizations/{id}载荷新增必填orgCode(机构编码可编辑),保存时查重(排除自身、仅比对未删除机构),冲突返回409并提示"机构编码已存在";创建语义不变。OpenAPI同步升级为1.15.0。

版本1.16经统领会话批准,移除角色"可访问部门"维度:①GET/PUT /api/v1/roles/{id}/departments两个接口下线,sys_role_dept表删除(迁移0021_remove_assigned_scope / Flyway V6__remove_assigned_scope);②DeptScope枚举收敛为OWN/OTHER,"其他部门"涵盖所属部门子树之外的全部部门(含无归属文档);③权限矩阵由每角色15行(5密级×3部门范围)收敛为10行(5密级×2部门范围),存量dept_scope='ASSIGNED'矩阵行物理删除;④UI字典deptScopes移除ASSIGNED选项。OpenAPI同步升级为1.16.0。

版本1.17经统领会话批准,管理类权限矩阵化(分类/机构/用户编辑权限入角色):①sys_role新增manage_category_edit/manage_org_edit/manage_user_edit三管理位与managed_org_id管理范围锚点(NULL=全局),角色创建/编辑接口(POST/PUT /api/v1/roles)载荷新增对应四个字段;②判定读"生效角色"(用户显式角色活跃且启用时优先,否则回退默认角色),仅ADMIN类别生效:分类编辑全局生效,机构/用户编辑受锚点部门及其下级子树(沿启用边BFS)约束,无权返回403 FORBIDDEN("无分类/机构/用户编辑权限"或"目标超出管理范围");锚点接受特殊值"SELF"(落库哨兵0,0非合法机构ID),判定时锚点跟随各用户自身organization_id(即用户身居哪个部门就管该部门子树,用户无部门时管理范围为空集),角色与用户接口序列化时0还原为"SELF";③内置admin账号恒为全权超管,不受三管理位与范围约束,登录与用户详情的permXxx/managedOrgId四字段对超管恒返回全开/全局(不因其无显式角色回退默认角色而归零);④迁移0023_admin_permissions(Flyway V8)先落用户级过渡列并初始化存量(ADMIN默认三权限开启、锚点=所在部门),0024_manage_permission_matrix(Flyway V9)将存量用户级配置收敛上收到生效角色(多组占用时克隆角色追加-M{n}后缀并回填),随后删除sys_user用户级四列;⑤UserSummary新增permCategoryEdit/permOrgEdit/permUserEdit/managedOrgId管理位展示(四字段值均取自生效角色,字段名沿用过渡期命名),用户创建/更新接口不再接受管理位字段(载荷携带返回400 INVALID_ARGUMENT)。OpenAPI同步升级为1.17.0。

版本1.18经统领会话批准,新增跨页调整顺序两个接口(拖动重排仅限当前页内,跨页移动由行级"移动"入口承载):①PUT /api/v1/documents/{documentId}/move——管理员/受权用户将主案或配套方案移动到浏览范围内指定位置,载荷为{targetPosition}与{target:"TOP"|"BOTTOM"}二选一(非法组合返回400 INVALID_ARGUMENT),主案可携带可选scopeCategoryId(分类子树语义,配套方案携带时返回400);②PUT /api/v1/main-plans/{mainPlanId}/attachments/{bindingId}/move——移动主案挂载附件到页签类型范围内指定位置,载荷必填mainPlanRowVersion(乐观锁,过期返回409 DATA_VERSION_CONFLICT),可选scopeAttachmentType限定附件类型范围。位置为范围内1-based序号并自动收敛到[1,范围内总数];重排采用"可见子序列重排+槽位回填",范围外条目保持原相对顺序;目标位与当前位相同时为无操作(不写库、不审计),响应文档移动{position,total}、挂载移动{position,total,mainPlanRowVersion}(有实际移动时主案rowVersion加1并回传)。权限校验与拖动重排一致(文档EDIT / 主案BIND_ATTACHMENT,设密主案同过密码锁);文档不在指定分类/类型范围内返回409 DOCUMENT_NOT_IN_MOVE_SCOPE(新增错误码)。审计动作新增MOVE_DOCUMENT(目标DOCUMENT)与MOVE_ATTACHMENT(目标ATTACHMENT_BINDING),UI字典auditActions同步两词条;sys_audit_log的action_type CHECK白名单同步扩展至38项(迁移0025_move_audit_actions / Flyway V10__move_audit_actions,达梦下约束名为大写CK_SYS_AUDIT_LOG_AUDIT_ACTION_TYPE,探测删除后重建)。前端管理页主体方案表与挂载附件表新增"移动"行操作及位置对话框(数字目标+置顶/末尾快捷,成功后跳转目标页)。OpenAPI同步升级为1.18.0。

版本1.19经统领会话批准,权限模块整体改为借阅授权语义:①授权界面三种可见范围(ALL_AUTHENTICATED/ORGANIZATION/CUSTOM)全部下线,只保留面向用户的限时授权——操作位收敛为canView/canDownload两项,expiresAt必选且最长7天(Z结尾UTC,服务端60秒容差,到期由读路径惰性判定失效);②GET/PUT /api/v1/documents/{id}/permissions载荷随之收敛(详见第12章),保存请求字段固定为documentRowVersion+entries(每条仅subjectId/canView/canDownload/expiresAt),保存不触碰审批单凭证;③每条授权可选挂一份PDF审批单凭证,新增POST/GET/DELETE /api/v1/documents/{id}/permissions/entries/{entryId}/voucher三个接口(上传为替换语义,旧文件在DB事务成功后清理;仅主案可操作,校验口径与授权保存一致),审计动作新增UPLOAD_PERMISSION_VOUCHER/DELETE_PERMISSION_VOUCHER(目标PERMISSION,UI字典auditActions同步两词条,sys_audit_log.action_type CHECK白名单扩至40项);④DocumentSummary的visibilityType+visibilitySummary改为borrowSummary(生效借阅人姓名、连接,无生效借阅为未配置,无查看权限列表行为-),DocumentDetail.permissionSummary改为{borrowerCount, inheritedFromMainPlan}(与摘要同口径统计生效借阅人数);⑤GET /api/v1/documents移除visibilityType查询参数;⑥数据库迁移0026_borrow_permission(FlywayV11__borrow_permission,双引擎幂等互冲):doc_permission物理删除全部ORG主体行,删列subject_type/can_edit/can_manage_permission/can_delete,唯一约束收敛为(document_id, subject_id, active_marker),新增expires_at(NOT NULL,1970哨兵fail-safe,存量活跃USER行初始化为迁移时刻+7天)与凭证四列voucher_file_name/voucher_storage_path/voucher_file_size/voucher_uploaded_at;doc_document删除visibility_type列(shape约束重建为v3),存量"全员可见/组织可见"文档回退角色权限矩阵控制;⑦恢复语义调整:授权还原仅面向用户主体(失效主体跳过计数),保留原expiresAt(已到期条目不构成失败),错误码RESTORE_PERMISSION_INVALID废止;⑧UI字典移除visibilityTypes/subjectTypes两组(13组),权限主体枚举(3.8节)整体废止。前端授权弹窗改为借阅语义(借阅人/查看/下载/到期时间/审批单列,datetime-local分钟粒度,max=当前+7天,到期条目标记"已到期",凭证行级上传/下载/替换/清除)。OpenAPI同步升级为1.19.0`。

版本1.20经统领会话批准,新增界面主题(账号级漫游):①新增PUT /api/v1/users/me/ui-theme——本人界面主题偏好,任意登录用户可改自己、无需管理权限,载荷为{uiTheme}白名单blue/red/green(非法值返回400 INVALID_ARGUMENT,载荷未知字段严格拒绝),成功返回最新UserResponse;主题为纯视觉偏好:不校验rowVersion(不干扰并发编辑乐观锁)、不bump authVersion(不使Token失效下线会话)、不写审计日志;②UserSummary(登录、users/me、用户管理列表与详情)新增必返字段uiTheme,存量与缺省为blue;③前端登录/门户设密/会话恢复三入口以后端账号偏好对齐本地主题(后端为权威源,本地localStorage仅作防闪烁缓存——index.html内联脚本在首帧前按缓存设置html[data-theme],登录后被账号值覆盖);④数据库迁移0027_ui_theme(Flyway V12__ui_theme,双引擎幂等互冲):sys_user加ui_theme VARCHAR(12) NOT NULL DEFAULT 'blue',CHECK白名单blue/red/green(convention渲染名ck_sys_user_user_ui_theme;达梦约束名为大写,两端探测均大小写不敏感比较、DROP用库中实际名);达梦下Flyway默认关闭(DM不在其支持列表),另附等价手工脚本db/migration-dm/V12__ui_theme.sql(DMSQL匿名块+EXECUTE IMMEDIATE+USER_TAB_COLUMNS/USER_CONSTRAINTS探测,与db/migration平级目录不参与Flyway扫描);⑤前端主题基础设施:theme.css新增--dms-deep/--dms-bright/--dms-canvas及--dms-*-rgb透明度族语义变量与--chart-6,html[data-theme="red"|"green"]覆盖块整体换肤(background/primary/chart/sidebar等全套设计token),全站硬编码主色(含App.tsx色板常量与各组件rgba(…)透明色族约220处)变量化迁移至CSS变量;例外不迁移:密级徽章、安全水印、审计扇形多彩色板、destructive警示色(内容语义色不随主题变化);⑥前端顶栏新增主题切换入口(调色板下拉,三色样高亮当前项),切换本地立即生效并异步PUT同步账号,保存失败提示但本地不回滚。OpenAPI同步升级为1.20.0。

版本1.21经统领会话批准,安全加固三项:①访问令牌有效期默认由7200秒收敛为3600秒(1小时)并配置化——环境变量DMS_JWT_EXPIRES_SECONDS(双版同名,非法值启动即失败),keepSignedIn=true的7天保持不变;②敏感操作二次认证——新增POST /api/v1/auth/reauth(登录态重输登录密码换取5分钟reauth令牌,claims含type=reauth并绑定sub与authVersion,不可冒充访问令牌,authVersion变化即失效),敏感接口携带X-DMS-Reauth-Token请求头校验,缺失/无效/过期/非本人/版本不符一律返回新增错误码403 REAUTH_REQUIRED("敏感操作需要二次认证,请重新输入登录密码");覆盖接口为:PUT /api/v1/users/{id}(仅当securityLevel/roleCode/roleId/status实际变化时强制)、PUT /api/v1/documents/{id}/permissions(借阅授权保存含授予与撤销)、POST与DELETE /api/v1/documents/{id}/permissions/entries/{entryId}/voucher(审批单凭证上传与清除)、PUT /api/v1/roles/{id}/permissions(角色权限矩阵保存);前端在对应操作提交前弹出登录密码确认框,令牌内存缓存5分钟内免重复输入;③应用层信封加密——新增环境变量DMS_STORAGE_KEK(base64编码32字节AES-256密钥),配置后original/新落盘文件按DMSE+kekVersion(1B)+包裹DEK长度(4B)+KEK包裹的DEK+密文格式以AES-256-GCM加密(DEK每文件随机32B、nonce 12B),读路径按DMSE头自动识别密文/明文实现明文兼容,未配置KEK时维持明文落盘;存量文件不回迁加密,预览转换/正文提取等读路径经统一解密入口(密文解出临时明文、用后清理)。OpenAPI同步升级为1.21.0。

版本1.22经统领会话批准,数据备份定版:①备份管理5接口(GET/PUT /api/v1/backup/config、POST /api/v1/backup/jobs(202受理)、GET /api/v1/backup/jobs/current(进度快照)、POST /api/v1/backup/jobs/current/cancel、GET /api/v1/backup/jobs(历史清单)),配置含目标模式(LOCAL本机目录/RSYNC守护进程rsync://user@host:873/module/专用协议推送,含密码认证与--delete镜像清理)、定时开关与时刻、全量间隔天数、保留链数;②全量/增量链式备份覆盖达梦dexp逻辑导出、original/preview/extracted/quarantine/recycle/voucher六个存储目录与.env配置副本,增量基于最近全量差异传输;③审计动作新增BACKUP_CREATE(目标BACKUP,迁移0028_backup_audit/Flyway V13,含达梦适配版);④离线恢复CLI(Pythondms-server restore-backup/Java--restore-backup)按清单恢复存储文件,.env存为.env.restored不覆盖当前配置。OpenAPI同步升级为1.22.0。

版本1.23经统领会话批准,系统管理:①角色新增两管理位manage_system/manage_audit_log(sys_role加列,迁移0029_system_management/Flyway V14双引擎幂等,含达梦适配版),角色创建/编辑载荷与回显字段名为manageSystemManage/manageAuditLog;UserSummary新增permSystemManage/permAuditLog(内置admin恒true);②模块推导——AUDIT_LOG改由manage_audit_log推导、新增SYSTEM_MANAGEMENT由manage_system推导(内置admin恒可见),浏览/后台管理/机构用户管理推导不变;③鉴权调整——备份6接口与system/**新接口改系统管理位判定、审计5接口与system/logs/download改日志审计位判定(原ADMIN硬判定废止,admin恒过);④新增/api/v1/system/**共12接口:metrics/current(实时CPU/内存/交换/磁盘/开机时间)、metrics/history?hours=(1~720缺省24,采样表sys_metric_sample30秒间隔保留7天)、process(进程CPU/内存/线程数/运行时长,pythonVersion字段Java版返回JDK版本)、storage-usage(七存储子目录遍历统计)、converter(LibreOffice存活/并发占用/累计成功失败计数,内存态重启清零)、health(数据库/存储可写/转换器/备份目标/信封加密五项体检)、info(契约版本/后端类型PYTHON|JAVA/运行时/OS/数据库类型/启动时间/关键路径)、runtime-config(15项运行参数脱敏总览,密钥类仅显示已配置/未配置)、logs?lines=&level=(运行日志尾部1~5000行缺省500,按[LEVEL]标记过滤)、logs/download(原始日志text/plain附件)、maintenance/preview-cache(POST清空预览缓存,审计PREVIEW_CACHE_CLEAR)、users/{user_id}/force-offline(POST提升authVersion强制下线,自保护拒绝本人,审计FORCE_OFFLINE);⑤新增POST /api/v1/backup/test-target(备份目标连通性探测,审计BACKUP_TEST_TARGET,探测失败返回{ok:false,error}不报错);⑥新配置DMS_LOG_FILE(运行日志文件,缺省logs/dms-server.log,10MB滚动×5,OFF/NONE关闭)、DMS_METRIC_INTERVAL_SECONDS(缺省30,下限10)、DMS_METRIC_RETENTION_DAYS(缺省7,下限1);⑦Python版资源采集依赖psutil(缺失时监控接口返回503 METRIC_UNAVAILABLE),Java版用JDK内建MXBean零依赖;审计动作CHECK白名单扩至42项、目标类型白名单扩至12项(新增SYSTEM)。OpenAPI同步升级为1.23.0。

版本1.24经统领会话批准,门户设密自选机构:①POST /api/v1/auth/portal/verify的SET_PASSWORD阶段响应新增organizations(本系统启用机构下拉,元素{id,orgCode,orgName,orgPath},id为字符串,按orgPath稳定排序)与suggestedOrganizationId(按对端orgName推断的预选机构ID字符串,推断不出为null)——本系统按需同步用户、不自动同步机构树,两系统机构树可能不一致,由用户设密时自选所在机构;②POST /api/v1/auth/portal/setup-password载荷新增可选organizationId(字符串,须指向有效且已启用机构,非法返回400 INVALID_ARGUMENT"organizationId必须指向有效且已启用的机构",非字符串/空串拒绝),设密成功时用户机构同步更新为所选机构并记入LOGIN审计detail(仅实际变化时记录organizationId);缺省该字段(旧客户端兼容)不改变用户机构;③前端设密界面在机构列表非空时渲染必选"所在机构"下拉(预选suggestedOrganizationId,未选提交提示"请选择所在机构"),列表为空时跳过不强制;④Python与Java双版实现逐条对齐(Java侧updatePortalPassword单UPDATE合并机构列,保持单次row_version+1与Python ORM单写一致);⑤无数据库结构变更、无新增路径。OpenAPI同步升级为1.24.0。

版本1.25经统领会话批准,本地单点登录代理(仅Java版):①新增POST /api/v1/auth/portal/proxy-verify(无认证、无请求体)——令牌取自部署机本机单点登录代理(Java后端经sso-sdk:UserClient.getInstance(host,port).getToken()取token,UserService.getInstance(host,port).getUserBasicInfo(token)以token换用户基本信息userId/username/realName,对端不提供机构字段恒为null),随后的按需同步建档与双阶段判定和门户跳转验签完全共用同一实现,响应结构与portal/verify逐字段一致(LOGIN/SET_PASSWORD双阶段,首登返回一次性设密票据与机构自选列表);②仅Java版后端提供本接口,Python版不实现(请求返回404)、功能未启用返回400 INVALID_ARGUMENT"单点登录代理未启用",前端在未登录且地址栏无门户query时打开页面自动静默探测一次,探测失败(404/400/代理不可达/令牌无效)静默回落账号密码登录页,成功则进入与门户跳转一致的双阶段界面;③新配置项(仅Java版消费):DMS_SSO_PROXY_ENABLED(开关,缺省false)、DMS_SSO_PROXY_HOST/DMS_SSO_PROXY_PORT(代理地址,缺省127.0.0.1:8080)、DMS_SSO_USER_SERVICE_HOST/DMS_SSO_USER_SERVICE_PORT(用户服务地址,缺省127.0.0.1:8800)、DMS_SSO_TIMEOUT_SECONDS(缺省10);sso-sdk以system scope依赖引入(backend-java/lib/sso-sdk-0.1.0.jar,打包经includeSystemScope进入fat jar),当前为mock实现(源码见仓库mock-sso-sdk/,配套mock代理tools/mock-sso-proxy/mock_sso_proxy.py),真实SDK到货后替换jar即可,SsoProxyGateway为唯一封装点;④审计沿用门户链路(CREATE_USER/EDIT_USER/LOGIN,detail含portal:true),设密机构自选沿用1.24契约;⑤无数据库结构变更。OpenAPI同步升级为1.25.0。

版本1.26经统领会话批准,监控信息展示开关:①新增配置DMS_SYSTEM_MONITOR_ENABLED(双版同名,接受1/true/yes/on大小写不敏感,缺省false);②GET /api/v1/system/info响应新增必填布尔字段monitoringEnabled(SystemInfoData同步加列),双端实现逐条对齐;③为false时前端系统管理页隐藏"维护操作"区块之上的全部监控信息(系统信息、应用进程、实时资源、资源采样趋势、存储目录用量、文档转换器、运行参数总览),并跳过对应监控接口请求,仅保留操作条、维护操作与数据备份区域;接口本身不下线、不追加权限判定;④GET /api/v1/system/runtime-config总览新增"系统/监控信息展示"条目回显当前值(启用/停用);⑤无数据库结构变更、无新增路径。OpenAPI同步升级为1.26.0。

版本1.27经统领会话批准,列表拖动重排:①新增三个重排接口——PUT /api/v1/organizations/reorder(载荷{parentId,orderedIds},parentId为null表示顶层机构排序,否则为该父机构直接子级排序,上级不存在返回404、机构超出管理范围返回403)、PUT /api/v1/users/reorder(载荷{orderedIds})、PUT /api/v1/roles/reorder(载荷{orderedIds}),orderedIds为拖动后的目标顺序(正整数字符串ID数组、无重复、至少2个,否则400 INVALID_ARGUMENT;含列表外的无效ID同样400);②重排采用槽位置换语义:被提交ID按原槽位次序排好依次填入目标顺序,未提交行相对次序不变(分页部分提交安全);变更行按步长10重编号sort_no且row_version加1,sort_no无实际变化的行不写库,响应统一为{reorderedCount}(实际写库行数);③权限——机构/用户reorder要求ADMIN并分别校验机构编辑位/用户编辑位(机构受管理范围锚点约束),角色reorder要求ADMIN且角色编辑位,与各自编辑接口一致;④审计动作新增REORDER_ORGANIZATION/REORDER_USER/REORDER_ROLE(UI字典auditActions同步三词条,sys_audit_log.action_type CHECK白名单扩至45项,detail含orderedIds/reorderedCount,机构另含scope=parentId或"ROOT");⑤数据库迁移0031_user_sort_no(Flyway V16__user_sort_no,双引擎幂等互冲,含达梦适配版):sys_user加sort_no INT NOT NULL DEFAULT 0(机构/角色已有同名列),用户列表排序由real_name,id改为sort_no,id,存量用户按id序展示;⑥前端机构树同级拖拽、用户列表与角色列表行拖拽排序接入三个接口。OpenAPI同步升级为1.27.0。

版本1.28,IP条目范围形式:①IP条目ipValue在精确IP与无主机位CIDR网段之外新增起止IP范围形式(如192.168.1.1-192.168.1.254,连接符为半角减号,仅允许一个);②两端各自按精确IP规则解析(IPv4拒绝前导零与非法段、IPv6字面量),要求两端同为IPv4或同为IPv6(否则400 INVALID_ARGUMENT「IP范围两端必须同为IPv4或IPv6:原文本」),起始不得大于结束(否则400「IP范围起始不能大于结束:原文本」),规范化存储为「规范化起文本-规范化止文本」;多余连字符、空端点等非法格式统一提示「IP地址、网段或IP范围格式非法:原文本」(原「IP地址或网段格式非法」文案同步扩展);③登录IP校验同步支持:来源地址与范围同版本且落在任一分配条目的[起,止]闭区间(含两端边界)即放行;④无数据库结构变更(复用ip_value文本列,规范化结果超过64字符上限时按长度超限400拒绝),存量精确IP/CIDR条目行为不变;⑤前端防手误确认:录入/编辑起止范围时,若两端前三段(IPv4 C类网段)不一致,提交前弹窗二次确认并提示匹配IP总数(止−起+1,千分位展示),取消则不提交;纯前端交互,后端仍接受跨网段范围,无接口变更。OpenAPI同步升级为1.28.0。

版本1.29,页面空闲自动登出与机构树默认语义:①登录(POST /api/v1/auth/login)与门户设密完成(POST /api/v1/auth/portal/setup-password)响应data新增必填整数字段idleLogoutMinutes(≥0)——取环境变量DMS_IDLE_LOGOUT_MINUTES(双版同名,非负整数、非法值启动即失败,缺省5),0表示关闭;reauth响应语义不同不携带;②前端登录/设密/会话恢复后将该值随令牌按keepSignedIn同策略持久化(localStorage或sessionStorage,退出登录清除),页面监测鼠标移动/按键/滚动/触摸/滚轮/点击活动,连续无活动达到配置分钟数后自动调用logout退出并固定提示「长时间未操作,已自动退出登录」(15秒周期检查,触发一次后不再重复,重新登录重新计时);③GET /api/v1/organizations/tree查询参数status缺省语义修正——不传时返回全部机构(含停用)(原实现缺省仅返回启用机构导致停用机构从管理列表消失),传ENABLED/DISABLED仍按值过滤,过滤时停用节点保留其祖先链;④无数据库结构变更、无新增路径。OpenAPI同步升级为1.29.0。

版本1.30,用户级管理范围:①sys_user新增managed_org_id三态列(迁移0032_user_managed_org,Flyway V17__user_managed_org,双引擎幂等互冲安全、含达梦适配版):NULL=全局、哨兵0=SELF(跟随本人所在机构,存量回填0、换机构自动跟随)、机构ID=固定锚点(含子树,沿启用边BFS);②用户创建(POST /api/v1/users)与更新(PUT /api/v1/users/{id})载荷新增可选字段managedOrgId(null/空串=全局、"SELF"=跟随所在机构、机构ID须指向未删除机构否则400):create省略时落SELF(哨兵0),update省略时保持原值不变(显式null=设为全局);该字段调整不改变认证身份——不触发reauth、不递增authVersion,仅随常规编辑递增rowVersion并计入EDIT_USER审计before/after;③UserSummary.managedOrgId语义改回用户级三态(机构ID/"SELF"/null,内置admin恒null),不再取生效角色锚点;④范围计算规则——实际生效范围以生效角色锚点为准;角色锚点为"SELF"(哨兵0)时回落用户级设置:用户NULL=全局、用户SELF=其所在机构子树(无部门则空集)、用户机构ID=该锚点子树;内置admin恒全局。管理范围对机构/用户编辑与用户列表收敛生效,分类/角色编辑不受锚点约束;用户列表口径修订——不传organizationId时仍静默收敛到管理范围子树(范围空集返回空页),显式请求的organizationId(含后代展开)与管理范围无交集时返回403 FORBIDDEN("无权限查看该机构的用户"),不再静默交集返回空页,避免前端误显示"该机构无用户"。⑤移动权限口径修订(修订1.18)——只有对文档有EDIT权限的用户才可"移动"文档:主案/子案移动与拖动重排维持文档自身EDIT判定不变;主案挂载附件的移动(PUT /main-plans/{id}/attachments/{bindingId}/move)与拖动重排由主案BIND_ATTACHMENT改判主案EDIT(调整主案挂载区顺序属主案编辑范畴,与挂载/解挂的BIND_ATTACHMENT/UNBIND_ATTACHMENT解耦),无权返回403 FORBIDDEN;前端管理页主体方案表"移动"按钮按行级allowedActions无EDIT置灰(悬停提示原因),挂载附件表"移动"与拖动按所选主案EDIT表级控制;⑥用户编辑防提权——POST /api/v1/users与PUT /api/v1/users/{id}新增目标生效范围约束:仅当目标roleCode=ADMIN且其生效角色具备机构/用户编辑位(manageOrgEdit/manageUserEdit 任一)时校验,目标最终生效管理范围(按「角色锚点优先,角色SELF回落用户级三态」解析)不得超出操作者管理范围——解析为全局(角色锚点或用户级为NULL)或范围外机构返回403 FORBIDDEN("目标超出管理范围");无管理位角色(含默认角色)与USER类别目标不受限,update未提交managedOrgId时按保持原值判定,校验先于二次认证执行(换角色场景先报越权而非REAUTH_REQUIRED);内置admin作为操作者恒全局不受限。OpenAPI同步升级为1.30.0。

版本1.31,矩阵上传位与主题背景图:①角色方案权限矩阵新增上传位——RolePermissionRow布尔位扩为8个(canView/canUpload/canDownload/canEdit/canDelete/canConfigPermission/canBindAttachment/canUnbindAttachment,canUpload位列canView之后),canUpload仅OWN行有意义、OTHER行恒false(提交OTHER行为true返回400 INVALID_ARGUMENT「上传权限仅可设置本部门方案」);迁移0033_role_upload_permission(Flyway V18__role_upload_permission,双引擎幂等互冲,含达梦适配版)为sys_role_permission加can_upload列(存量行回填false,需上传的角色由管理员显式开启);②上传判定——POST /api/v1/documents与POST /api/v1/documents/batch-import按矩阵上传位校验:内置admin恒可,其余用户按方案归属部门取生效角色对应行(本部门OWN行、他部门OTHER行恒拒)的canUpload,无权返回403 FORBIDDEN;③UserSummary新增必填布尔字段permUpload(生效角色矩阵任一OWN行canUpload为true即true,内置admin恒true)——该位为前端入口门控(上传/批量导入按钮显隐),服务端仍按②逐文档精确判定;前端界面矩阵列更名「权限矩阵」→「方案权限」,canBindAttachment/canUnbindAttachment界面合并为单一「挂载」开关(提交两列同值,接口字段与语义不变);④登录IP限制豁免收窄(修订1.10)——机构分配IP条目后仅内置admin账号豁免,原ADMIN类别整体豁免废止,其余管理员与普通用户同样受所在机构IP限制;⑤角色锚点简化——角色维护界面移除「机构及用户管理范围」下拉、恒按「跟随用户设置」(SELF)提交,迁移0034_role_anchor_self(Flyway V19__role_anchor_self,双引擎幂等,含达梦适配版)将存量角色锚点全部回填为SELF(哨兵0),生效管理范围统一按1.30④用户级三态解析;⑥登录背景图按主题——GET /api/v1/config/login-background新增可选查询参数theme(枚举blue/red/green,白名单外取值按未提供处理),提供时优先返回运行目录下background-{theme}.jpg/png(jpg优先),主题图缺失回落通用background.jpg/png;前端按html[data-theme]当前主题请求并按主题独立缓存。OpenAPI同步升级为1.31.0。

版本1.32,登录图片验证码(安全需求4):①新增公开接口GET /api/v1/auth/captcha——开关开启时返回{enabled: true, captchaId, image}(image为data:image/png;base64前缀的132×44 PNG,4位字符剔除易混淆0O1I,120秒有效),关闭时仅返回{enabled: false};②开关取环境变量DMS_CAPTCHA_ENABLED(双版同名,接受1/true/yes/on,缺省true即默认启用);③开启时POST /api/v1/auth/login请求体扩为严格五键(追加captchaId/captchaCode,忽略大小写与首尾空白校验),关闭时保持三键不得携带;④校验先于用户查询与锁定检查(防爆破第一道闸),不写审计、不计入登录失败锁定;验证码一次性消费(无论登录成败即作废),缺失/错误/过期/未知captchaId统一返回新增错误码400 INVALID_CAPTCHA(「验证码错误或已过期」,不区分具体原因);⑤验证码内存存储(单实例部署,签发上限1000条防刷量,过期惰性清理);⑥前端登录页加载时探测GET /auth/captcha,enabled=true展示验证码输入与图片(点击图片刷新),登录失败自动刷新;无数据库结构变更。OpenAPI同步升级为1.32.0。

版本1.33,超级管理员隐藏与机构树管理范围裁剪(安全需求7/8):①新增环境变量DMS_ALLOW_ADMIN_LOGIN(双版同名,接受1/true/yes/on,缺省true允许)——设为false后三处行为同时生效:内置admin登录一律按用户名不存在处理(查库前短路,报错、审计、锁定行为与密码错误完全一致,不泄露账号存在);GET /api/v1/users用户列表隐藏内置admin账号(分页total同步扣减);POST /api/v1/users创建用户名admin返回400 INVALID_ARGUMENT(「不能创建该名称的用户」,不再报「用户名已存在」,先于唯一性查重)。②机构树管理范围裁剪——GET /api/v1/organizations/tree按请求者生效管理范围(1.30④解析规则)裁剪:锚点管理员仅返回管理范围子树内机构(森林形式,根从锚点机构起,范围外机构与其祖先链不出现,keyword/status过滤在裁剪后的集合内进行),全局管理员(内置admin与锚点解析为NULL/全局)不受限返回全量;机构管理页、用户管理页与用户编辑「所属机构」下拉均消费本接口,裁剪后各处仅见有管理权限的机构树。③修复双端不一致:DMS_CAPTCHA_ENABLED=false时Python版登录仍被验证码校验拦截(INVALID_CAPTCHA)无法登录的问题,开关关闭时直接放行校验(对齐Java版,登录回归三键契约)。无数据库结构变更。OpenAPI同步升级为1.33.0。 版本1.34,站内通知:①新增sys_notification表(id/user_id/title/content/notify_type/action/status/created_at/handled_at,追加型无软删无乐观锁,Alembic 0036_notification 与 Flyway V21__notification 双引擎幂等建表,含达梦适配版);②新增四个接口(详见6A章,均需Bearer认证、仅操作本人通知)——GET /api/v1/notifications当前用户通知列表(id倒序、最近200条)、GET /api/v1/notifications/unread-count未读数(前端用户名红圈x与面板「通知」红圈数据源)、POST /api/v1/notifications/read-all全部已读(打开通知页清红圈,响应updated为本轮标记条数)、POST /api/v1/notifications/{id}/handle待办处理(请求体{"action":"AGREE"|"REJECT"},仅action=PENDING且归属本人可处理,处理后置READ并记handledAt,重复处理或NONE返回409,他人/不存在返回404);③action枚举NONE/PENDING/AGREE/REJECT、notify_type枚举当前仅APPROVAL(CHECK白名单约束),已读(红圈清零)与已处理(同意/拒绝出结果徽标)两个状态正交;④通知为审批流等待办的呈现通道,本版无写入方(列表恒空、红圈不显示),后续审批流直接向sys_notification插入PENDING记录即可点亮前端。OpenAPI同步升级为1.34.0。

版本1.35,菜单显示方式:①UserSummary.allowedModules推导口径全面修订——菜单可见性完全由用户生效角色的权限决定(纯矩阵/管理位判定),与用户类别(USER/ADMIN仅为助记符)无关、不再查询文档存在性;②方案页签——方案权限矩阵含管理类列(上传/编辑/删除/授权/挂载/解除挂载)任一,或具备分类管理位:渲染BACKEND_MANAGEMENT且不再渲染DOCUMENT_BROWSER;否则矩阵含浏览类列(查看/下载)任一:渲染DOCUMENT_BROWSER;③管理页签——日志审计管理位渲染AUDIT_LOG、系统管理管理位渲染SYSTEM_MANAGEMENT、机构/用户/角色管理位任一渲染ORGANIZATION_USER_MANAGEMENT(页签内按钮按权限细分,管理位仅ADMIN类别角色生效的现状不变);④矩阵与管理位全空(角色未分配任何权限):兜底渲染DOCUMENT_BROWSER;⑤内置admin恒全量四页签(无方案浏览);⑥双端删除「按已发布主案存在性判定管理菜单」旧推导(Python has_any_manage_action/Java hasAnyManageAction与existsManageHit),改为has_any_matrix_permission纯矩阵判定;页签内按钮与接口鉴权维持现状按实时判定。无数据库结构变更、无新增路径。OpenAPI同步升级为1.35.0。

版本1.36,操作位完全拆分:①allowedActions与写路径鉴权(require_plan_action/requirePlanAction)口径修订——各操作位独立判定,「查看」不再是其他操作位的前置:原实现未命中查看(矩阵无canView且无生效借阅ACL查看)时allowedActions整体为空集、写接口一律403「无权{操作}该文档」;修订后矩阵操作位(编辑/删除/授权/挂载/解除挂载)命中即独立下发与放行,无查看权仅影响查看类能力(在线预览、详情描述/正文片段/借阅摘要/正文检索命中仍按查看权拦截与抹除);②下载位同步独立——PlanAccess.canDownload语义由「查看且可下载」改为「可下载」:矩阵仅勾下载位(未勾查看)、借阅ACL仅勾下载均直接生效,allowedActions含DOWNLOAD且下载接口放行(原矩阵下载要求矩阵同时含查看、ACL下载要求同时命中查看);③共享附件访问聚合同步拆分——任一有效挂载主案命中下载位即可下载该附件(原要求该主案同时可查看);④写路径错误消息收敛——无操作位统一403「无权{操作}该文档:角色权限未授予该操作」(原无查看时先报「无权{操作}该文档」);⑤密级前置、USER类别未发布状态闸、子案操作位过滤(无授权/挂载位)等既有约束维持不变;⑥双端同步改造(Python plan_allowed_actions/require_plan_action/下载鉴权/附件聚合,Java planAllowedActions/requirePlanAction/authorizeDownload/evaluateAttachmentAccess),「按钮可点 ⟺ 后端放行」不变式保持。无数据库结构变更、无新增路径。OpenAPI同步升级为1.36.0。

版本1.37,借阅申请审批流:①借阅授权由手工配置改为申请-审批流——前端隐藏手工「借阅授权」按钮(12.1-12.5接口与逻辑保留待切换),主案行改设「借阅」按钮,仅当申请人对该文档既无查看又无下载权限且密级足够时亮起(后端同口径守卫);②新增申请单两表(Flyway V22__borrow_request,含达梦适配版)——doc_borrow_request(申请单:文档/申请人/到期时间/审批单凭证四列/状态PENDING-APPROVED-REJECTED/处理人/时间)与doc_borrow_request_item(条目:借阅人/查看位/下载位),sys_notification加biz_id列(APPROVAL待办指向申请单);③新增三接口(详见12.6章)——POST /api/v1/documents/{id}/borrow-requests提交申请(multipart:request part为JSON字符串{entries,expiresAt}+可选file part为PDF审批单,一批用户共用一张;敏感操作需二次认证;重复待审批申请返回409 BORROW_REQUEST_DUPLICATE,成功响应{requestId,notified})、GET /api/v1/borrow-requests/{id}申请详情(申请人本人或文档授权位可见)、GET /api/v1/borrow-requests/{id}/voucher审批单下载(权限同详情);④审批经站内通知联动——提交即向所有CONFIG_PERMISSION用户发APPROVAL+PENDING待办(NotificationSummary新增bizId指向申请单,通知待办操作列新增「查看审批单」入口),任一审批人在通知中同意/拒绝即定(findForUpdate行锁+状态守卫,后到者409「该借阅申请已被处理」),notifications/{id}/handle对APPROVAL待办升级为敏感操作需X-DMS-Reauth-Token;同意逐条目落地doc_permission(同12.2单条目语义),拒绝不落地;⑤审计动作新增BORROW_REQUEST_CREATE/BORROW_REQUEST_APPROVE/BORROW_REQUEST_REJECT(目标BORROW_REQUEST,UI字典auditActions/auditTargets同步词条);⑥前端新增借阅申请弹窗(本人固定首行+可添加他人、查看/下载双勾默认全选、到期时间默认+7天上限7天、可选上传PDF审批单)与申请详情弹窗(只读+审批单下载),通知待办处理经二次认证包装。OpenAPI同步升级为1.37.0。

版本1.38,一次一码轮换(安全需求:写操作防重放,仅 Java 版实现):①读写分离——GET/HEAD 读请求沿用原授权码不轮换不比对码代(REST 读重放无危害,轮询与写并发时在途读请求不掉线);POST/PUT/DELETE/PATCH 写请求严格比对当前会话代(sys_user.session_jti = JWT jti),业务成功(响应<400)后服务端轮换出新授权码经响应头X-DMS-Access-Token返回,客户端以之作下一次请求的授权码——旧码对写操作立即失效,写请求携旧码返回新增错误码401 SESSION_ROTATED(「授权码已更换,请重新登录」);②轮换的新码继承原码剩余有效期与全部身份声明(不重置时长、不改变authVersion);并发写由服务端 CAS(UPDATE ... WHERE session_jti = 旧码)保证任意响应乱序下客户端拿到的都是最新码,前端无需串行化;③sys_user加session_jti列(VARCHAR(64) NULL,Alembic 0038_session_jti 与 Flyway V24__session_jti 双引擎幂等迁移,含达梦适配版)——NULL = 未纳管(存量会话/开关关闭期间登录/Python版签发),比对与轮换跳过,旧令牌自然过渡到重新登录后纳管;④新增环境变量DMS_TOKEN_ROTATION_ENABLED(双版同名,接受1/true/yes/on,缺省true启用);关闭时登录落 NULL 不纳管、写操作不比对不轮换;⑤登录与门户设密完成响应data新增必填布尔字段tokenRotation(开关回显,前端据此启用写操作响应头换码;Python 版不实现轮换,恒回显false对齐双端契约);⑥前端tokenStore新增rotateAccessToken(按当前存储位置原位替换),HTTP客户端单出口在响应返回时同步读取X-DMS-Access-Token头替换本地授权码;写响应彻底丢失(网络中断/页面关闭)时客户端持旧码,下一次写 401 重新登录(一次一码固有代价)。无新增接口路径。OpenAPI同步升级为1.38.0。

版本1.39,禁用用户名密码登录(安全管控,仅 Java 版实现):①新增环境变量 DMS_PASSWORD_LOGIN_DISABLED(双版同名,接受1/true/yes/on,缺省 false);②开启后 POST /auth/login 对全部用户(含内置 admin)最前置 403 FORBIDDEN(「用户名密码登录已被禁用,请使用单点登录」)——守卫先于验证码校验与用户查询,不消耗验证码、不泄露账号是否存在、不计入锁定;③门户跳转验签(6.4)与本地 SSO 代理登录(契约1.25)不受影响,为开关开启后仅存的登录入口;④GET /auth/captcha 响应 data 新增必返布尔 passwordLoginDisabled 回显(开关开启/关闭两分支均携带),前端据此隐藏登录页账号密码表单、仅显示单点登录提示(隐藏仅为体验,后端守卫为安全兜底,接口直调同样被拒);⑤.env.example 补充配置说明与「开启前须确认门户(DMSPORTAL*)或本地 SSO(DMS_SSO_PROXY_ENABLED)至少一项可用,否则无人能登录」警示。无数据库结构变更、无新增接口路径。OpenAPI 同步升级为 1.39.0。

20. Q3-B 1.4接口补充

  1. 正式RoleCode仅为ADMIN、USER。四个审计查询接口与文档统计接口均要求ADMIN。历史AUDITOR不能登录,旧Token必须因authVersion或用户状态校验返回401。
  2. GET /api/v1/documents新增可选布尔参数includeDescendants。非法布尔值返回400 INVALID_ARGUMENT。仅在传入categoryId时true表示包含该分类全部有效后代;缺省或false为精确分类;未传分类时查询当前用户全部可见MAIN。
  3. MAIN创建metadata仅接受documentName、documentType=MAIN、categoryId、securityLevel、summary、tags。SUB_PLAN创建仅接受documentName、documentType=SUB_PLAN、parentDocumentId、securityLevel、summary、tags。未知字段严格拒绝。单文件和批量导入一致。
  4. MAIN编辑仅接受documentName、summary、categoryId、securityLevel、tags、rowVersion;SUB_PLAN编辑不含categoryId。两者均不接受status或visibilityType。
  5. 非叶子分类用于MAIN创建或编辑时返回409 CATEGORY_NOT_LEAF,details至少包含字符串categoryId和整数childCategoryCount。
  6. GET /api/v1/attachments对所有登录用户开放且全量可见(支撑全局检索按标题命中全部附件);普通用户返回的allowedActions为空集,且不返回description与contentSnippet;上传、编辑、删除等写操作仍仅ADMIN。附件详情、统一preview/download路径对USER执行“至少一个有效挂载主案及对应VIEW/DOWNLOAD权限”检查。
  7. GET /api/v1/documents/{id}/preview的200响应支持application/pdf与application/vnd.openxmlformats-officedocument.wordprocessingml.document,返回原文件并携带inline Content-Disposition、X-Content-Type-Options、Cache-Control和X-Request-Id。DOC、XLS、XLSX返回统一JSON 415 PREVIEW_UNAVAILABLE。
  8. keyword继续检索名称、摘要和标签,并扩展检索数据库中的正文组合字段;LIKE通配字符必须转义,过滤在分页前执行,不在查询时逐文件读取磁盘。
  9. 本版本不新增路径;OpenAPI info.version为1.4.0,迁移为0003_q3_business_alignment_and_content_search。