# 方案计划文档管理系统接口契约 > 契约版本:1.20 > 状态:已确认,LibreOffice 离线文档转换与 PDF 预览接口基线 > 外部字段风格:`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-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` 无需认证。 请求: ```json { "username": "admin", "password": "password", "keepSignedIn": true } ``` 成功: ```json { "code": "OK", "message": "success", "data": { "accessToken": "jwt", "tokenType": "Bearer", "expiresIn": 7200, "user": {} }, "requestId": "uuid" } ``` `user` 使用 `UserSummary`。用户名或密码错误统一返回 `401 INVALID_CREDENTIALS`,不得泄露用户是否存在。 Token有效期固定为: - `keepSignedIn=false`:`expiresIn=7200`; - `keepSignedIn=true`:`expiresIn=604800`。 第一阶段不签发刷新令牌。 ### 6.2 `POST /api/v1/auth/logout` 请求体为空。成功后递增当前用户 `authVersion`,使该用户全部旧Token失效。 ### 6.3 `GET /api/v1/users/me` 返回当前 `UserSummary`。 ## 7. 组织和人员 ### 7.1 `GET /api/v1/organizations/tree` 查询参数: - `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`) 只允许对 `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` + 密级 + 密码锁)。 - 请求为 `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 相同。 - 条目未上传凭证返回 `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`。 ## 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`。