# 方案计划文档管理系统接口契约 > 契约版本:1.33 > 状态:已确认,登录图片验证码基线 > 外部字段风格:`camelCase` > 数据库字段风格:`snake_case` ## 1. 使用规则 本文件是 `/api/v1` 的唯一人工可读接口契约。执行会话必须完整读取 `DMS_FUNCTION_CONTRACT.md` 和本文件。 后续建立的 `backend/openapi/openapi.yaml` 是本文件的机器可读镜像,不得另行发明字段、路径或枚举。两者不一致时不得自行选择,必须提交统领会话裁决。 以下旧路径明确废止,不得实现,也不得由前端调用: ```text /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` 永久废止,由以下独立回收站资源替代,不提供重定向、别名或兼容调用: ```text GET /api/v1/recycle-bin/documents POST /api/v1/recycle-bin/documents/{id}/restore ``` ## 2. 通用约定 ### 2.1 基础路径与认证 ```text Base path: /api/v1 Authorization: Bearer X-Request-Id: ``` 除登录和健康检查外,所有接口需要有效 Token。 敏感操作另需二次认证:请求携带 `X-DMS-Reauth-Token: `(由 `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: ```text X-Request-Id: ``` ### 2.2 标识符与时间 - 数据库主键使用 `BIGINT`; - JSON 中所有 ID 使用十进制字符串,禁止返回 JavaScript 不安全的大整数; - 时间统一存储为 UTC; - JSON 时间使用带 `Z` 的 ISO 8601,例如 `2026-07-23T03:00:00.000Z`; - 客户端显示时转换到本地时区。 ### 2.3 JSON成功响应 ```json { "code": "OK", "message": "success", "data": {}, "requestId": "8ed3e305-73a8-4af7-8f39-1b86b9a2dad2" } ``` 创建成功使用 HTTP `201`,其他普通成功使用 `200`。删除成功仍返回上述 JSON,不使用 `204`。 ### 2.4 分页数据 ```json { "items": [], "page": 1, "pageSize": 20, "total": 0, "totalPages": 0 } ``` 默认值: - `page=1` - `pageSize=20` - 最大 `pageSize=100` ### 2.5 错误响应 ```json { "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`。版本不一致返回: ```text HTTP 409 code = DATA_VERSION_CONFLICT ``` 成功修改后返回新的 `rowVersion`。 ### 2.8 Blob响应 预览和下载成功时不使用 JSON 包装。 PDF预览: ```text Content-Type: application/pdf Content-Disposition: inline; filename*=UTF-8''... X-Request-Id: ... ``` 下载: ```text Content-Type: 实际MIME类型或application/octet-stream Content-Disposition: attachment; filename*=UTF-8''... X-Request-Id: ... ``` 文件接口失败时返回普通 JSON 错误响应。 ## 3. 固定枚举 ### 3.1 角色 ```text USER ADMIN ``` ### 3.2 模块 ```text DOCUMENT_BROWSER BACKEND_MANAGEMENT AUDIT_LOG ``` ### 3.3 文档类型 ```text MAIN SUB_PLAN ATTACHMENT ``` ### 3.4 文档状态 ```text DRAFT PUBLISHED ARCHIVED ``` ### 3.5 密级 ```text PUBLIC INTERNAL SECRET CONFIDENTIAL TOP_SECRET ``` ### 3.6 可见范围 ```text ALL_AUTHENTICATED ORGANIZATION CUSTOM ``` ### 3.7 附件类型 ```text POLICY WORK_STANDARD TABLE DIAGRAM OTHER ``` ### 3.8 权限主体 (1.19 起废止:借阅授权仅面向用户主体,`doc_permission.subject_type` 列与 `ORG/USER` 枚举一并下线,接口与数据库均不再存在主体类型维度。) ### 3.9 可执行操作 ```text VIEW DOWNLOAD EDIT CONFIG_PERMISSION DELETE BIND_ATTACHMENT UNBIND_ATTACHMENT RESTORE_DOCUMENT ``` ### 3.10 分类类型 ```text SCENE STYLE SITUATION VERSION OTHER ``` ### 3.11 启停状态 用户、组织和分类统一使用: ```text ENABLED DISABLED ``` ### 3.12 审计结果 ```text SUCCESS FAILURE ``` ### 3.13 审计动作 ```text 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 审计目标 ```text AUTH CATEGORY DOCUMENT ATTACHMENT PERMISSION ATTACHMENT_BINDING ``` ## 4. 公共DTO ### 4.1 UserSummary ```json { "id": "1", "username": "admin", "realName": "系统管理员", "organizationId": "10", "organizationName": "机关", "roleCode": "ADMIN", "securityLevel": "TOP_SECRET", "status": "ENABLED", "allowedModules": [ "DOCUMENT_BROWSER", "BACKEND_MANAGEMENT" ] } ``` 模块映射(`allowedModules`)按“用户对文档的实际权限”动态计算: - `USER`:始终包含 `DOCUMENT_BROWSER`;当角色权限矩阵对任一已发布主案命中管理位 (编辑/删除/授权/挂载/解除挂载,且该主案对用户可查看)时,追加 `BACKEND_MANAGEMENT` - `ADMIN`:`DOCUMENT_BROWSER`、`BACKEND_MANAGEMENT`、`AUDIT_LOG`、`ORGANIZATION_USER_MANAGEMENT` 前端必须使用后端返回的 `allowedModules`,不得根据角色自行补全模块。 ### 4.2 CategoryNode ```json { "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 ```json { "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` 与摘要同口径统计生效借阅人数。 ### 4.4 DocumentDetail `DocumentDetail` 包含 `DocumentSummary` 的全部字段,并增加: ```json { "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 ```json { "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 ```json { "document": {}, "restoredPermissionCount": 2, "skippedPermissionCount": 1, "restoredBindingCount": 3, "skippedBindingCount": 1 } ``` `document`使用 `DocumentDetail` 字段,但恢复响应必须通过纯序列化生成,不得增加 `viewCount`、写入 `VIEW_DOCUMENT` 审计或触发文档详情查询的查看副作用。 ## 5. 健康检查 ### `GET /api/v1/health` 无需认证。 ```json { "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`): ```json { "username": "admin", "password": "password", "keepSignedIn": true, "captchaId": "uuid", "captchaCode": "A3BK" } ``` 验证码关闭时请求体保持三键(`username`/`password`/`keepSignedIn`,不得携带验证码字段);开启时必须且只能包含上述五键。 成功: ```json { "code": "OK", "message": "success", "data": { "accessToken": "jwt", "tokenType": "Bearer", "expiresIn": 3600, "idleLogoutMinutes": 5, "user": {} }, "requestId": "uuid" } ``` `user` 使用 `UserSummary`。用户名或密码错误统一返回 `401 INVALID_CREDENTIALS`,不得泄露用户是否存在。 验证码校验(契约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,会话恢复后沿用。 ### 6.2 `POST /api/v1/auth/logout` 请求体为空。成功后递增当前用户 `authVersion`,使该用户全部旧Token失效。 ### 6.3 `POST /api/v1/auth/reauth` 敏感操作二次认证(契约1.21新增)。登录态下重输登录密码,换取短时 reauth 令牌。 请求体(严格单键): ```json { "password": "login-password" } ``` 成功: ```json { "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)。无需认证。 成功(开关开启): ```json { "code": "OK", "message": "success", "data": { "enabled": true, "captchaId": "uuid-hex", "image": "data:image/png;base64,..." }, "requestId": "uuid" } ``` 开关关闭时 `data` 仅含 `{"enabled": false}`,登录页隐藏验证码输入、登录请求体保持三键。 规则: - 开关取环境变量 `DMS_CAPTCHA_ENABLED`(双版同名,接受 1/true/yes/on,缺省 `true` 即默认启用); - 图片 132×44 PNG(data URL,`img` 标签 `src` 直接使用),4 位字符(剔除易混淆 `0O1I`),忽略大小写校验; - 有效期 120 秒,超时未消费自动清理; - **一次性消费**:登录校验后(无论成败)即作废; - 响应带 `Cache-Control: no-store`,前端不得缓存;验证码获取失败静默隐藏输入框不阻塞登录流程(后端可能未开启)。 ## 7. 组织和人员 ### 7.1 `GET /api/v1/organizations/tree` 查询参数: - `keyword`:可选,组织名称或编码; - `status`:可选,按状态过滤;不传时返回全部机构(含停用,契约1.29),停用节点仍保留其祖先链以便展示树形层级。 响应按请求者生效管理范围裁剪(契约1.33):锚点管理员仅返回管理范围子树内的机构(森林形式,根从锚点机构起,范围外机构与其祖先链不出现,`keyword`/`status`过滤在裁剪后的集合内进行);全局管理员不受限,返回全部机构。机构管理页、用户管理页与用户编辑「所属机构」下拉均消费本接口。 返回组织树数组,每个节点至少包含: ```json { "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`。 ## 8. 分类接口 ### 8.1 `GET /api/v1/categories/tree` 查询参数: - `keyword`:只搜索分类名称或编码; - `status`:可选。 返回 `CategoryNode[]`。 `documentCount` 表示该分类完整有效子树内所有有效主体方案(MAIN)数量,含本分类与全部后代分类,不按当前用户密级/权限过滤,所有登录用户口径一致;计数沿父链穿过停用分类节点累加。 ### 8.2 `POST /api/v1/categories` 管理员接口。 ```json { "categoryCode": "SCENE_A", "categoryName": "XXXX场景A", "categoryType": "SCENE", "parentId": null, "sortNo": 10 } ``` 返回新建 `CategoryNode`,HTTP `201`。 ### 8.3 `PUT /api/v1/categories/{id}` 管理员接口。 ```json { "categoryName": "新名称", "categoryType": "SCENE", "parentId": null, "sortNo": 20, "rowVersion": 1 } ``` 移动分类不能形成环。成功返回更新后的 `CategoryNode`。 ### 8.4 `DELETE /api/v1/categories/{id}` 查询参数: ```text rowVersion= ``` 存在有效子分类或有效主案、子方案时返回 `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` 页签调用规则: ```text 主案列表: 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`。 ### 9.2 `POST /api/v1/documents` 管理员接口,`multipart/form-data`: - `file`:一个文件; - `metadata`:JSON字符串。 主案 metadata: ```json { "documentName": "主案名称", "documentType": "MAIN", "summary": "概述", "categoryId": "100", "securityLevel": "SECRET", "tags": ["标签"] } ``` 子方案 metadata 增加: ```json { "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`: ```json { "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`) ```json { "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`) 查询参数: ```text rowVersion= ``` 主案存在有效子方案时返回 `409 MAIN_PLAN_HAS_CHILDREN`。成功返回: ```json { "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`。`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`,只包含有效 `ATTACHMENT`。 该列表对所有登录用户开放且全量可见(支撑全局检索按标题命中全部附件)。普通用户返回的 `allowedActions` 为空集,且不返回 `description` 与 `contentSnippet`;上传、编辑、删除等写操作仍仅 ADMIN,附件内容访问由详情、预览、下载接口按挂载主案权限独立拦截。 ### 10.2 `POST /api/v1/attachments` 管理员接口,`multipart/form-data`: - `file` - `metadata` ```json { "documentName": "附件名称", "attachmentType": "POLICY", "summary": "说明", "tags": ["制度"] } ``` 后端强制: ```text 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`;共享附件仅管理员可维护): ```json { "documentName": "新名称", "attachmentType": "WORK_STANDARD", "summary": "新说明", "tags": ["规范"], "rowVersion": 1, "organizationId": "10" } ``` 不得修改附件固定密级、固定可见范围和文件身份。`organizationId` 可选,语义与9.5一致:缺省沿用现有归属, 调整到其他部门仅 `ADMIN` 可执行。 ### 10.6 `DELETE /api/v1/attachments/{id}` 查询参数: ```text rowVersion= ``` 存在有效挂载时: ```text HTTP 409 code = ATTACHMENT_IN_USE details.mountedPlanCount details.mainPlans ``` ### 10.7 `GET /api/v1/attachments/{id}/main-plans` 返回: ```json { "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`,增加: ```json { "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`): ```json { "attachmentIds": ["2001", "2002"], "mainPlanRowVersion": 3 } ``` 请求内重复ID自动去重;已经挂载的附件不重复创建。 ```json { "createdCount": 1, "existingCount": 1, "attachmentCount": 5, "mainPlanRowVersion": 4 } ``` ### 11.3 `DELETE /api/v1/main-plans/{id}/attachments/{attachmentId}` 按文档权限校验:需通过密级检查且具备该文档对应操作权限(管理员恒通过;普通用户由角色矩阵×密级×部门范围与文档授权 OR 叠合决定)。设密主案另需通过对应操作密码锁。(主案 `UNBIND_ATTACHMENT`) 查询参数: ```text mainPlanRowVersion= ``` 逻辑删除挂载关系,不删除附件。返回最新附件计数和主案版本。 ## 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`) 主案返回: ```json { "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`。 ```json { "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`)。 成功返回: ```json { "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`)。 成功返回: ```json { "documentId": "1001", "entryId": "7001" } ``` ## 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`,所有ID均为字符串。 ### 14.2 `POST /api/v1/recycle-bin/documents/{id}/restore` 请求体必须且只能包含: ```json { "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恢复后强制保持: ```text 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` 返回分页日志: ```json { "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` ```json { "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`。 返回: ```json { "items": [ { "period": "2026-07-23", "operationCount": 25 } ] } ``` ### 15.4 `GET /api/v1/audit/statistics/actions` 参数:`createdFrom`、`createdTo`。 返回动作分布: ```json { "items": [ { "actionType": "VIEW_DOCUMENT", "count": 20 } ] } ``` ### 15.5 `GET /api/v1/audit/statistics/users` 参数:`createdFrom`、`createdTo`、`limit`,`limit` 默认10、最大100。 返回活跃用户: ```json { "items": [ { "userId": "1", "realName": "系统管理员", "organizationName": "机关", "operationCount": 15 } ] } ``` ## 16. 标准错误码 实现可以增加更细错误码,但不得改变以下语义: ```text INVALID_ARGUMENT INVALID_CREDENTIALS TOKEN_INVALID TOKEN_EXPIRED USER_DISABLED AUTH_VERSION_MISMATCH 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 查询接口 ```text GET /api/v1/config/ui-dictionaries ``` - 认证:有效登录用户,`ADMIN`、`USER`均可访问。 - 请求参数:无。 - 成功:`200`,统一响应中的 `data` 为 `UiDictionaries`。 - 失败:无Token或失效Token返回统一 `401`;启动期配置错误导致应用启动失败,运行期意外错误使用统一 `500`。 - 该接口不产生业务审计,字典读取本身不访问数据库,不提供任何配置写接口。 `DictionaryOption`: ```json { "code": "INTERNAL", "label": "内部", "sortNo": 20 } ``` `UiDictionaries`: ```json { "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`(Flyway `V11__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(Python`dms-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_sample`30秒间隔保留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`。 ## 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`。