# 方案计划文档管理系统接口契约 > 契约版本:1.73 > 状态:已确认,登录图片验证码基线 > 外部字段风格:`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。 一次一码轮换(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: ` 返回,客户端以之替换本地存储作为下一次请求的授权码——旧码对写操作立即失效; - 写请求携带已轮换的旧码返回 `401 SESSION_ROTATED`("授权码已更换,请重新登录"),处理方式与其他 401 一致(清令牌、引导重新登录); - 轮换的新码继承原码剩余有效期与全部身份声明(不重置时长、不改变 `authVersion`);并发写由服务端 CAS 保证任意响应乱序下客户端拿到的都是最新码,前端无需串行化; - `session_jti` 为 NULL(存量会话、开关关闭期间登录、Python 版签发)时跳过比对与轮换,旧令牌自然过渡到重新登录后纳管;开关关闭期间登录不落会话代。 敏感操作另需二次认证:请求携带 `X-DMS-Reauth-Token: `(由 `POST /api/v1/auth/reauth` 重输登录密码换取,5 分钟有效,见 6.3)。缺失、无效、过期、非本人或 `authVersion` 不符返回 `403 REAUTH_REQUIRED`。覆盖接口: - `PUT /api/v1/users/{id}`:仅当 `securityLevel`、`roleCode`、`roleIds`(挂载角色集合)、`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", "roleId": "3", "roleName": "系统管理员", "roles": [ { "id": "3", "roleName": "系统管理员" }, { "id": "5", "roleName": "方案审核" } ], "seatName": "1号席位", "userType": "NORMAL", "securityLevel": "TOP_SECRET", "status": "ENABLED", "allowedModules": [ "DOCUMENT_BROWSER", "BACKEND_MANAGEMENT" ] } ``` 多角色与用户字段口径(契约1.46): - `roleCode` 仍是**用户类别**(`ADMIN`/`USER`,决定权限体系归属),用户可挂载多个功能角色(`sys_user_role` 多对多,`roleId`/`roleName` 单值列停用不删); - `roleId`/`roleName` 展示口径:本人(`GET /users/me`、登录响应)=**会话选定角色**(JWT `roleId` claim 对应),非本人(用户列表行)=第一挂载角色(按 `sortNo` 排序); - `roles`:全部**活跃启用**挂载角色简表(`{id, roleName}`),登录选择框与编辑表单回显同口径,可为空数组; - `seatName`:席位,可空字符串或 `null`(≤64字符),前端右上角按「机构 + 空格 + 席位」展示; - `userType`:用户类型,`NORMAL`=普通用户、`COMMANDER`=首长——首长仍属 `USER` 类别(`roleCode=USER`),仅重置密码方式后续差异化。 模块映射(`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 ```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` 与摘要同口径统计生效调阅人数。 `allowedActions` 各操作位**独立判定**(1.36 完全拆分):「查看」不再是其他操作位的前置——矩阵勾选任一操作位(含未勾查看)即独立下发该位,写接口同口径放行;`DOWNLOAD` 由矩阵下载位或调阅ACL下载位独立命中(不再要求同时可查看)。查看类能力(在线预览、详情描述/正文片段、调阅摘要、正文检索命中)仍要求查看权。 ### 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`,不得携带验证码字段);开启时必须且只能包含上述五键。可选键 `roleId`(契约1.46 多角色):数字字符串(须匹配 `^[0-9]+$`),与验证码开闭正交(关闭时三或四键、开启时五或六键),非法值返回 `400 INVALID_ARGUMENT`("roleId必须是数字字符串")。 多角色选择步骤(契约1.46):密码/验证码/锁定/IP 限制全部通过后,若用户挂载**多个活跃启用角色**且未携带 `roleId`,不签发令牌、不记登录成功,改返回选择步骤: ```json { "code": "OK", "message": "success", "data": { "roleSelectionRequired": true, "username": "zhangsan", "realName": "张三", "roles": [ { "id": "3", "roleName": "方案浏览" }, { "id": "5", "roleName": "方案管理" } ] }, "requestId": "uuid" } ``` 前端展示角色选择框,用户选定后**携带 `roleId` 重新提交同一登录接口**(密码与验证码重新校验,两轮各消费一张验证码);携带的 `roleId` 不在挂载列表内返回 `400 INVALID_ARGUMENT`("roleId必须指向已挂载的角色")。仅挂载一个活跃启用角色时自动选中(无需第二轮),无挂载角色时 `roleId` claim 为 `null`(会话走默认角色,与 `effectiveRole` 回落口径一致)。会话锁定单角色:选定的 `roleId` 写入 JWT `roleId` claim(数字),全程生效,切换角色需退出重新登录。 成功: ```json { "code": "OK", "message": "success", "data": { "accessToken": "jwt", "tokenType": "Bearer", "expiresIn": 3600, "tokenRotation": true, "idleLogoutMinutes": 5, "user": {} }, "requestId": "uuid" } ``` `user` 使用 `UserSummary`(`roleId`/`roleName` 为本次会话选定角色)。用户名或密码错误统一返回 `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 令牌。 请求体(严格单键): ```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, "passwordLoginDisabled": false, "captchaId": "uuid-hex", "image": "data:image/png;base64,..." }, "requestId": "uuid" } ``` 开关关闭时 `data` 为 `{"enabled": false, "passwordLoginDisabled": <开关当前值>}`,登录页隐藏验证码输入、登录请求体保持三键。 规则: - 开关取环境变量 `DMS_CAPTCHA_ENABLED`(双版同名,接受 1/true/yes/on,缺省 `false` 即默认关闭——1.40 起缺省由 `true` 改为 `false`); - 图片 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 条): ```json { "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`过滤在裁剪后的集合内进行);全局管理员不受限,返回全部机构。机构管理页、用户管理页与用户编辑「所属机构」下拉均消费本接口。 只读放开(契约1.45):本接口不再要求ADMIN类别,任意登录用户可调。普通用户(USER)恒返回本人所在部门为根的子树(沿 ENABLED 机构边展开,无部门返回空树)——与7.2用户列表收敛口径一致;ADMIN 类别维持既有管理范围裁剪不变。 返回组织树数组,每个节点至少包含: ```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`。 只读放开(契约1.45):本接口不再要求ADMIN类别,任意登录用户可调。范围口径——普通用户(USER)不传机构时静默收敛到本人所在部门子树(含下级按 `includeDescendants` 展开,沿 ENABLED 机构边,无部门返回空页);显式请求机构不在本人部门子树返回 `403`「无权限查看该机构的用户」(与锚点管理员同口径);ADMIN 类别维持既有管理范围收敛不变。返回 DTO 不变(全量 `UserSummary`)。 ## 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` 按契约1.43/1.44附件位下发:矩阵附件查看/下载/编辑/删除位命中即下发对应位(不叠加挂载主案权限,附件不跟随主案权限),无任何命中为空集,且不返回 `description` 与 `contentSnippet`;附件上传按矩阵附件上传位(10.2),编辑按 ADMIN 或矩阵附件编辑位(10.5),删除仍仅 ADMIN,附件内容访问由详情、预览、下载接口按「挂载主案权限 OR 矩阵附件位」独立拦截(密级前置不变)。矩阵附件位取行(契约1.59):共享资料库行组(`top_category_id IS NULL` × 类型 × 密级,OWN)对**全部**该类型附件生效(含已挂载,兜底放权),与挂载主案一级分类行任一命中即放行——分类下附件矩阵行已隐藏并清零(迁移 V33),类型页签为统一配置入口,分类行保留日后恢复配置的加细能力)。 ### 10.2 `POST /api/v1/attachments` 按矩阵附件上传位校验(契约1.43):仅内置超管(username=admin)恒可,其余用户(含受限ADMIN)按附件密级取生效角色 OWN 行 `can_attachment_upload`,无权返回 `403 FORBIDDEN`("无共享附件的上传权限")。上传指共享附件库中上传文档的权限,不含挂载操作(挂载仍走主案 `BIND_ATTACHMENT` 位)。`multipart/form-data`: - `file` - `metadata` ```json { "documentName": "附件名称", "attachmentType": "POLICY", "summary": "说明", "tags": ["制度"], "securityLevel": "PUBLIC" } ``` 后端强制: ```text documentType=ATTACHMENT categoryId=null ``` `securityLevel` 为必填枚举(契约1.42:开放附件密级,公开级为常态缺省);密级决定存储域(绝密走 `top-secret` 隔离域)与密级前置、OWN 行附件位判定。 返回 `DocumentDetail`,HTTP `201`。 ### 10.3 `POST /api/v1/attachments/batch-import` 与文档批量导入相同,使用重复 `files` 和顺序对应的 `items`。每个 item 使用附件 metadata 结构,每文件独立事务。鉴权与10.2一致(矩阵附件上传位,ADMIN 恒可)。 ### 10.4 `GET /api/v1/attachments/{id}` 返回附件 `DocumentDetail`。 ### 10.5 `PUT /api/v1/attachments/{id}` 按文档权限校验(契约1.43:`EDIT`——仅内置超管恒可,其余用户(含受限ADMIN)按附件密级取生效角色 OWN 行 `can_attachment_edit`,覆盖共享附件库与已挂载区域附件的编辑): ```json { "documentName": "新名称", "attachmentType": "WORK_STANDARD", "summary": "新说明", "tags": ["规范"], "securityLevel": "INTERNAL", "rowVersion": 1, "organizationId": "10" } ``` `securityLevel` 必填(契约1.42:可调整附件密级,跨域变更复用主案域迁移路径,调整绝密要求已配置KEK否则`409 STORAGE_KEK_REQUIRED`)。不得修改附件固定可见范围和文件身份。`organizationId` 可选,语义与9.5一致:缺省沿用现有归属, 调整到其他部门仅 `ADMIN` 可执行。 ### 10.6 `DELETE /api/v1/attachments/{id}` 按矩阵附件删除位校验(契约1.44):仅内置超管恒可,其余用户(含受限ADMIN)按附件密级取生效角色 OWN 行 `can_attachment_delete`,无权返回 `403 FORBIDDEN`。密级前置(密级不足先按密级拒绝)维持。 查询参数: ```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 叠合决定)。设密主案另需通过对应操作密码锁(契约1.41:解挂与挂载共用「挂载」锁位,操作码仍传 `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" } ``` ### 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`)。 ```json {"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}` 申请单详情:申请人本人或对该文档有授权权限的用户可见(审批人「查看审批单」)。返回申请单与条目列表: ```json { "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`,所有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 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 查询接口 ```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`。 版本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` 补充配置说明与「开启前须确认门户(DMS_PORTAL_*)或本地 SSO(DMS_SSO_PROXY_ENABLED)至少一项可用,否则无人能登录」警示。无数据库结构变更、无新增接口路径。OpenAPI 同步升级为 `1.39.0`。 版本1.41,**权限细分调整**:①方案权限矩阵新增**附件四权限位**——`RolePermissionRow`布尔位扩为12个(新增`canAttachmentUpload`/`canAttachmentEdit`/`canAttachmentView`/`canAttachmentDownload`),仅OWN行可设置(附件无部门归属,OTHER行提交true返回`400 INVALID_ARGUMENT`「附件权限仅可设置本部门行」,服务端恒拒真);迁移`0039_attachment_permissions_lock_merge`(Flyway `V25__attachment_permissions_lock_merge`,双引擎幂等互冲,含达梦适配版)为`sys_role_permission`加4列(存量行回填false);②**附件上传**——`POST /api/v1/attachments`与`POST /api/v1/attachments/batch-import`由仅ADMIN改为矩阵附件上传位判定(ADMIN类别恒可不收紧现状,USER按附件密级取OWN行`can_attachment_upload`,无权403 FORBIDDEN「无共享附件的上传权限」);③**附件编辑**——`PUT /api/v1/attachments/{id}`与已挂载区域附件编辑由仅ADMIN改为ADMIN或矩阵附件编辑位,普通用户`allowedActions`随之下发`EDIT`位;④**附件查看/下载不再跟随主方案**——附件访问评估在挂载主案权限聚合之外OR叠加矩阵附件查看/下载位:配置了附件查看/下载权限时,即使主案不具备任何权限其附件也可查看和下载;附件密级前置维持(密级不足附件位不生效);附件删除维持仅ADMIN;⑤`UserSummary`新增必返布尔字段`permAttachmentLibrary`(任一OWN行任一附件位命中或ADMIN类别恒true,共享资料库页签门控)与`permAttachmentUpload`(任一OWN行附件上传位命中或ADMIN类别恒true,共享附件上传/批量导入按钮门控);⑥**方案锁合并**——挂载与解除挂载合并为单一「挂载」锁位:设密`protectedActions`勾选位收敛为6项(`UNBIND_ATTACHMENT`不再是合法勾选码,提交返回400),勾选「挂载」即同保护挂载与解除挂载(解挂接口仍传`UNBIND_ATTACHMENT`操作码保持审计区分,服务端按挂载位判定);迁移V25同批将存量`protect_unbind_attachment=1`并入`protect_bind_attachment`(列保留兼容旧备份,代码不再读写);前端设密弹窗勾选项7→6、「挂载」位标签为「挂载/解除挂载」;⑦前端角色维护界面方案权限矩阵新增「附件上传/附件编辑/附件查看/附件下载」四列(仅本部门行可选)。OpenAPI同步升级为`1.41.0`。 版本1.42,**附件密级开放**:①共享资料(ATTACHMENT)创建metadata新增**必填**`securityLevel`(`AttachmentCreateMetadata`字段集由`[documentName, attachmentType, summary, tags]`扩为含`securityLevel`五键,`exactFields`全集校验拒绝缺失),上传、批量导入、编辑三入口一致;此前附件密级硬编码`PUBLIC`(创建恒公开、暂存域路由恒general),现按提交值落库与定存储域(绝密附件走`top-secret`隔离域);前端公开级为常态缺省(上传弹窗与批量导入行默认`PUBLIC`预置显式提交,普通用户仅需PUBLIC行上传位即可传公开件);②**附件编辑可调整密级**——`UpdateAttachmentRequest`新增必填`securityLevel`(`PUT /api/v1/attachments/{id}`),密级跨域变更复用主案编辑的域迁移路径(`migrateSecurityDomain`:调整绝密要求已配置KEK否则`409 STORAGE_KEK_REQUIRED`,原件搬迁失败整体回滚、预览缓存随密级迁移),审计before记录密级;`updateAttachmentMeta`落库新增`security_level`列(无表结构变更,列本就存在);③**上传鉴权按目标密级**——`canAttachmentUpload`按本次提交的`securityLevel`取OWN行上传位:上传SECRET附件需SECRET行`can_attachment_upload`(公开级缺省保证既有PUBLIC上传位用户不受影响);④附件访问判定(矩阵附件位、挂载聚合、密级前置)本就按附件自身密级取OWN行,密级开放后自动生效——密级调整为SECRET的附件立即按SECRET密级前置与OWN行附件位判定访问;存量附件恒PUBLIC无需数据迁移。OpenAPI同步升级为`1.42.0`。 版本1.43,**附件权限收口**(修复:受限管理员未配附件权限却全量可操作):①附件四权限(查看/下载/编辑/上传)判定中「ADMIN类别恒可」收口为**仅内置超管**(username=admin)恒可——受限ADMIN(自定义管理类角色,如方案管理员、安全管理员)与USER同口径按矩阵附件位判定,未配置附件位即无对应能力;②**附件查看/下载不再跟随挂载主案权限**——`evaluateAttachmentAccess`与`attachmentAllowedActions`移除挂载主案权限聚合路径,仅按附件密级取OWN行矩阵附件查看/下载位(对齐1.41需求原话「附件查看、下载不再跟随主方案」;原实现为OR叠加属理解偏差,主案权限命中不再放行附件);③附件编辑同口径(矩阵编辑位,超管恒可);附件删除维持ADMIN类别专属(含超管,受限ADMIN的`allowedActions`下发`DELETE`位);④`UserSummary`会话位`permAttachmentLibrary`/`permAttachmentUpload`去掉ADMIN类别恒true,仅超管恒true或矩阵位命中;前端共享资料库页签仅按`permAttachmentLibrary`渲染(不再叠加页面管理位);⑤写路径错误消息统一为「无权{操作}该共享附件:角色权限未授予该操作」(原「仅管理员可维护共享附件」);密级前置(任何角色含超管密级不足即拒绝)不变。无数据库结构变更、无新增路径。OpenAPI同步升级为`1.43.0`。 版本1.73,**重置密码入口改名申请重置密码与高级检索密级倒序**:①登录页与独立密码设密界面的「重置密码」按钮(打开契约1.49自助申请弹窗的两个入口)改名「申请重置密码」——实际行为是提交审批申请(首长由安全管理员、其他由系统管理员审批,通过后清密码回首次登录态),原名误导为立即重置;申请弹窗(ResetPasswordDialog)标题、aria-label、用户名placeholder(「请输入要申请重置密码的账号」)一并同步,提交按钮「提交申请」与成功提示「申请已提交,请等待审批」维持;用户管理页管理员的「重置密码」为直接重置语义(契约1.49仅首长收口走审批),不改名;②高级检索数据密级维度选项倒序展示——除「全部」置首外按绝密→机密→秘密→内部→公开(原为公开→绝密),实现为高级检索密级OptionGroup渲染前reverse(仅此处),UI字典sortNo(公开→绝密)与用户管理/文档表单等其他密级消费方顺序不变。纯前端界面优化,无接口变更,OpenAPI维持1.72.0。新增前端contract173测试(4用例:两入口按钮文案、弹窗三处文案、密级reverse且其他维度不受影响)。 版本1.72,**调阅审批单图片支持与上传提示删除**:①审批单凭证类型扩展——`stageVoucher`白名单由仅pdf扩为PDF+图片(jpg/jpeg/png/gif/bmp/webp,复用`UploadStorage.IMAGE_EXTENSIONS`):授权条目凭证上传(`POST /documents/{id}/permissions/entries/{entryId}/voucher`)与调阅申请file part共用同一暂存与校验口径,非白名单扩展名返回`415 UNSUPPORTED_FILE_TYPE`「审批单凭证只允许PDF或图片文件」(原「审批单凭证只允许PDF文件」);魔数校验复用`UploadStorage.validate`——扩展名与真实内容不一致按类型拒绝(如PDF扩展名装PNG内容报「PDF文件头与扩展名不一致」);落盘名由恒`{uuid}.pdf`改为`{uuid}.{真实扩展名}`,`StagedUpload.mimeType`由恒`application/pdf`改为`MIME_TYPES`按扩展名映射;凭证随密级分域落盘`{domain}/voucher/{uuid}.{ext}`的域路由不变;②审批单下载Content-Type按扩展名映射——新增`VoucherService.voucherMediaType`(jpg/jpeg→image/jpeg、png→image/png、gif→image/gif、bmp→image/bmp、webp→image/webp、未知回落application/pdf),`BorrowRequestService.downloadRequestVoucher`复用同一映射(此前授权凭证与申请审批单两处下载均硬编码application/pdf);③调阅申请空文件400文案改「调阅申请必须上传审批单(PDF 或图片)」(原「(PDF)」);④前端——文件选择accept由`application/pdf,.pdf`扩为`application/pdf,.pdf,image/*`;空审批单本地拦截文案同步「请上传调阅审批单(PDF 或图片)」;上传审批单处两处提示说明删除(选中态「一批用户共用一张,必传」与未选态「PDF,一批用户共用一张,可选」两个span一并移除),组件与borrowApi注释口径同步。无数据库结构变更、无新增接口路径。OpenAPI同步升级为`1.72.0`(borrow-requests与documents两处凭证/审批单的file描述、200响应content扩image/*、下载description改按扩展名映射说明)。新增`VoucherStageVoucherTest`(5用例:pdf/png/大写JPG通过且扩展名与MIME正确、txt拒绝、pdf扩展名装图片内容魔数拒绝)与前端contract172测试(4用例)。 版本1.71,**设密凭证已使用的自恢复出路(修复票据死锁交互)**:背景——设密票据一次性靠 authVersion+1 实现(consumeSetupTicket 比对用户当前 auth_version,设密成功即+1),弱网或前端异常下「后端设密已成功、前端未流转」时用户被困设密页:重试同一票据必报「凭证已使用」,旧文案「请从门户重新跳转」对本地重设密(契约1.65)场景完全无出路(无门户可跳)。修复双端:①后端 PORTAL_TICKET_INVALID 文案改为「设置密码凭证已使用——密码可能已设置成功,请返回登录页使用新密码重新登录」;②前端 PortalLaunchScreen.submitSetup 特判 HttpError.code=PORTAL_TICKET_INVALID——显示「密码可能已设置成功」提示并置 ticketUsed,主按钮下渲染「返回登录页,使用新密码登录」按钮(onFallback 回登录页,本地场景即清 localSetup),不再让用户反复重试注定失败的同张票据;③本地重设密场景设密页副标题由「首次从门户进入…」改为「密码已被重置,请为 X 设置新密码」。凭证已用即证明设密事务已提交——用户回登录页用新密码即可正常进入。新增 PortalTicketReuseTest(复用票据拒绝且文案含自恢复指引)与前端 contract171 测试(3 用例)。④根因修复(据用户 Network 抓包定位):setupPortalPassword 签发 token 先于内存对象 authVersion+1——token 声明旧 authVersion(如9)而 updatePortalPassword 落库 +1(如10),设密成功的响应 token 一落地即 AUTH_VERSION_MISMATCH,首个鉴权请求(ui-dictionaries)401 把用户踢回,这才是「设密成功却停在设密页」的第一击根因;修复为先同步内存对象(authVersion+1 等)再签发,PortalSetupRolePasswordTest 补断言 token 声明值为递增后值;⑤收尾——handlePortalSetupLogin 成功末尾清 localSetup:设密成功进入系统后退出登录,此前渲染调度仍优先命中设密页(票据已失效),现回正常登录页。无接口结构变更,OpenAPI维持1.66.0。 版本1.70,**修复审批重置密码违反非空约束(V36)**:契约1.49重置密码审批通过执行clearPasswordForReset(UPDATE sys_user SET password_hash = NULL 回首次登录态)与基线建表约束冲突——Alembic 0001建表sys_user.password_hash为NOT NULL(双库同源),达梦实测报「dm.jdbc.driver.DMException: 违反列[PASSWORD_HASH]非空约束」(严格模式MySQL同样会拒绝),该路径自1.49上线以来从未在真实库成功走过。修复为纯迁移V36(双引擎):sys_user.password_hash放开为可空——NULL=密码被审批清空待用户重设;应用层无需改动(本地登录判passwordHash null/空两态、门户验签按last_login_at判设密阶段、Passwords.verify对null回落DUMMY_HASH,均兼容)。达梦版migration-dm/V36以USER_TAB_COLUMNS.NULLABLE探测幂等,需在达梦库手工执行:ALTER TABLE SYS_USER MODIFY PASSWORD_HASH NULL;。同步修复V35达梦版脚本缺陷:DROP CONSTRAINT 的 DDL 误以模式名 DMS 作表名(ALTER TABLE DMS DROP CONSTRAINT…必报-2106「无效的表或视图名[DMS]」),且游标边遍历边 DDL 会因达梦 DDL 隐式提交使游标失效——重写为先 BULK COLLECT 收集(约束名,表名)成对入内存集合再逐条按表名删除。无接口变更,OpenAPI维持1.66.0。 版本1.69,**查看页与高级检索文案精简**:①查看页删除「正文已收起;在上方搜索框输入关键词…」提示行——正文区域仅保留三种状态渲染(正在加载正文…/未找到「关键词」/命中上下文摘要列表),无关键词时区域空白不再有说明文字;②高级检索折叠面板各维度选项组后的灰色解释说明删除——「数据内容·关键词的匹配范围」「数据分类·主体方案与共享资料类型」「提供单位·按机构及其下级过滤」三条hint移除,OptionGroup组件的hint属性(参数、类型、渲染span)随之删净,选项组仅保留标签与分段按钮。纯前端界面优化,无接口变更,OpenAPI维持1.66.0。 版本1.68,**移除检索结果统计行**:全局检索结果区(浏览页与管理页两处)不再显示「全局检索结果 · 共X项 / 主案X / 子案X / 共享资料X」统计横条——删除该行两个统计span与组件内孤立的searchStats useMemo(两处,按documentType分项计数逻辑随之移除);「命中超过1000条,仅展示前1000条」红色截断警示仍保留(改为仅截断时渲染该横条,非截断时不再出现空行边框),列表、分页、定位等其余行为不变。纯前端界面优化,无接口变更,OpenAPI维持1.66.0。 版本1.67,**查看全屏置顶搜索框(全屏检索不丢失)**:FilePreviewModal 的正文搜索框(输入框+命中计数+上一处/下一处按钮)由标题栏内联 JSX 抽为组件内共用变量searchBox,标题栏({isModal && searchBox})与全屏工具条(既有置顶横条「退出全屏/关闭」左侧)两处引用——此前进入全屏后标题栏整体隐藏、正文搜索随之不可用,需退出全屏才能继续检索;现全屏状态在置顶工具条即可输入关键词、查看「第N处/共M处」计数、上下跳转命中,与标题栏同源状态(keyword/totalCount/currentIndex 共享,Enter/Shift+Enter/Escape 快捷与清空行为一致),命中定位(文本侧scrollIntoView、PDF侧currentMatchIndex)与1.66命中摘要列表行为不变;搜索框 JSX 单一来源不重复维护;后续修复:全屏工具条容器补text-white——命中计数与上下箭头继承文字色,此前深色底上与背景同色不可见。纯前端界面优化,无接口变更,OpenAPI维持1.66.0。 版本1.66,**界面与流程五项优化(分类配色、调阅免认证、检索两行筛选、查看命中摘要)**:①**左侧一级分类多色循环**——一级分类Tab采用十色色板(红#C93B3B、蓝#2266C4、绿#2E8B57、橙#E07B1F、紫#8A4FBF、青#159A9C、棕#8A6D3B、粉#D4568F、灰#6B7280、深灰#4A5568)按页签序号取模循环区分,超出十色回绕;选中实底白字、未选中同色浅底(透明度26)同色深字、悬停加深;②**调阅申请免二次认证、审批单必传**——`POST /api/v1/documents/{documentId}/borrow-requests`提交不再要求X-DMS-Reauth-Token二次认证(reauth不再参与申请提交路径,审批动作handleApproval仍保留二次认证),审批单PDF改为必传——file缺失或空文件返回400 INVALID_ARGUMENT「调阅申请必须上传审批单(PDF)」;前端弹窗删除二次认证密码框、空文件先本地拦截提示「请上传调阅审批单(PDF)」,文案改「一批用户共用一张,必传」;③**高级检索数据分类两行**——第一行「数据分类」=主体方案(前端值plan:ALL,解析为planOnly=true仅查主案/子案方案路)+四类共享资料(att:{code}仅附件路);第二行为所有flatMode(仅主案模式、无共享资料挂载)一级分类(cat:{id}→categoryId仅方案路对应分类);第二行下拉不附加「全部」选项,两行全空=全部;documents/attachments列表接口本身不变(planOnly为前端组合行为);④**高级检索提供单位两行**——`GET /api/v1/organizations/filter-options`响应新增required字段childOrganizations(第N+1级启用机构清单,与organizations同排序规则sort_no升序null最后同值按id),第一行「提供单位」为organizations(第N级)、第二行「提供单位·下级机构」为childOrganizations(N=DMS_SEARCH_ORG_FILTER_LEVEL缺省2),第二行不附加「全部」;⑤**查看页收起正文、搜索显示命中摘要**——查看页不再渲染提取正文全文,原正文区域改为:无关键词时提示「正文已收起;在上方搜索框输入关键词,可在此查看各处命中的前后内容,点击跳转至对应位置」,有关键词时显示命中上下文摘要列表(每个命中前后各约30字符、归一化空白、未到开头/结尾以…省略、标注第N处命中、当前命中橙色描边高亮),点击条目经keywordSearch新增gotoIndex(index)按全局命中序号跳转(文本侧scrollIntoView、PDF侧currentMatchIndex定位);无命中显示「未找到「{keyword}」」。无数据库结构变更。OpenAPI同步升级为`1.66.0`。 版本1.65,**本地重设密(密码重置后本地登录直进设密界面)**:①背景——1.49重置密码审批通过后清密码回首次登录态,此前本地登录一律403「请通过单点登录进入并重新设置密码」,未启用单点登录的部署用户无法自助闭环;②**`POST /api/v1/auth/login`响应新增设密形态**——密码被清空的用户(`password_hash`为NULL,唯一来源为审批通过的密码重置)本地登录不再403:验证码/锁定/IP限制守卫照常通过后,**无论输入什么密码**都返回`SetupPasswordPhaseData`(与门户验签SET_PASSWORD同构:`{phase:"SET_PASSWORD", username, realName, portalTicket, organizations, suggestedOrganizationId, roles?}`),前端进入与单点登录首次设密**同一界面**重新设密,设密提交复用`POST /api/v1/portal/setup-password`(票据一次性、5分钟有效、authVersion防复用),多角色用户逐角色分配密码(1.53密码即角色);③安全口径——重置审批流即身份核实环节(首长→安全管理员、其他→系统管理员),验证码拦截自动化尝试,设密入口窗口仅存在于审批通过至本人设密之间,审计以reason=PASSWORD_RESET留痕;④实现——后端`PortalAuthService.buildSetupPasswordPayload`公共化(门户验签与本地登录共用,DRY)、`AuthService.login`密码空分支改发票据并前置IP限制校验,前端`authApi.login`守卫加`isSetupPasswordData`三态、`loginSession`透传不落token、`App`命中phase即切`PortalLaunchScreen`本地设密模式(`localSetup`属性跳过验签直进setup阶段);⑤新建用户密码仍由管理员设置(initialPassword),密码为空用户仅来自审批重置,分支覆盖面精确;后端257(+4)、前端522(+4)用例全过,OpenAPI升至1.65.0。 版本1.64,**数据库枚举约束全量清除**(决策:枚举合法性统一由应用端保证):①迁移`V35__drop_enum_check_constraints`(Flyway,含达梦适配版)一次性删除**全部20个**枚举型CHECK白名单约束——DOC_BORROW_REQUEST.STATUS、DOC_CATEGORY.STATUS/CATEGORY_TYPE、DOC_DOCUMENT.ATTACHMENT_TYPE_V2/CONTENT_EXTRACT_STATUS/SECURITY_LEVEL/DOCUMENT_STATUS/DOCUMENT_TYPE、SYS_AUDIT_LOG.OPERATION_RESULT、SYS_NOTIFICATION.ACTION/STATUS、SYS_ORGANIZATION.STATUS、SYS_PASSWORD_RESET_REQUEST.STATUS、SYS_ROLE_PERMISSION.DEPT_SCOPE_V2/SECURITY_LEVEL、SYS_ROLE.STATUS、SYS_USER.ROLE_CODE/SECURITY_LEVEL/STATUS/UI_THEME(V12小写名),延续V20/V23/V34三次同类修复(审计动作/审计对象/通知类型扩展时白名单遗漏同步、mock单测测不出库层约束、真实库操作连带500)的经验收敛;②应用端校验已确认覆盖——`Payloads.statusValue/roleCodeValue/securityLevelValue`(用户/机构/角色状态、角色类别、密级)、`CategoryService.categoryType`(分类五类型)、`UserService.UI_THEMES`(主题白名单)、`RoleService.validateMatrix`(部门范围/密级矩阵),其余枚举列(文档状态、正文提取态、附件类型、借阅/重置状态机、通知动作/状态)均为服务端常量写入不经外部输入;③**保留**三个跨列业务规则约束(非枚举型)——`CK_DOC_DOCUMENT_DOCUMENT_PASSWORD_MAIN_ONLY`(密码锁仅主案可设)、`CK_DOC_DOCUMENT_TYPE_SHAPE_V3`(三类文档形状一致性)、`CK_DOC_DOCUMENT_RELATION_DOCUMENT_RELATION_NO_SELF`(关联不得自指);④纯数据库迁移变更、无接口增删改,OpenAPI不升版(维持1.62.0);用户达梦库已直接执行并验证仅剩3个业务约束,MySQL环境由Flyway自动覆盖;后端253用例全过。 版本1.63,**重置密码申请500修复**:根因——`sys_notification.notify_type`列V21建表时的CHECK白名单仅`'APPROVAL'`(当时唯一通知类型),契约1.49新增通知类型`PASSWORD_RESET`(重置密码审批待办)未同步扩白名单,登录页「重置密码」提交申请时通知写入违约(达梦报「违反CHECK约束[CK_SYS_NOTIFICATION_NOTIFY_TYPE]」)、`GlobalExceptionHandler`兜底500「服务器内部错误」且事务回滚(申请单不落库、无PENDING残留);单测全程mock mapper测不出库层约束,故1.49上线至今未暴露。修复对齐V20/V23对`sys_audit_log`两列的先例——**通知类型为持续扩展枚举,删除库白名单约束、合法性收敛到应用层**:迁移V34(MySQL版`DROP CONSTRAINT`探测后删、达梦版`USER_CONSTRAINTS`游标+`EXECUTE IMMEDIATE`删,均幂等)删除`CK_SYS_NOTIFICATION_NOTIFY_TYPE`,`action`/`status`的CHECK保留(稳定小枚举);用户达梦库已直接执行删约束并按`createRequest`全SQL序列实测通过(申请单/通知/审计三写全绿、验证事务回滚不落库),MySQL库走Flyway自动迁移。接口签名零变更(OpenAPI不升版)。 版本1.62,**登录页文案可配置**:①**新增`GET /api/v1/config/login-branding`(公开)**——返回登录界面大标题与底部小字`{title, footerText}`,null=未设置(登录前无凭证访问,与登录背景图同模式的运行时配置;登录链路三个界面——账号密码登录页、门户启动页、认证过渡页——共用同一文案);②**新增`PUT /api/v1/config/login-branding`(`manage_system`管理位)**——请求体必含且仅含`title`/`footerText`(null/空白即清除恢复默认;非字符串400「{字段}必须是字符串」;超长400「登录页大标题不能超过64字」/「登录页底部小字不能超过200字」),两文案分别存`sys_setting`键值表`login.title`/`login.footer`行(先UPDATE后INSERT的通用upsert),响应回显`{title, footerText}`,写操作审计`LOGIN_BRANDING_UPDATE`(targetType=SYSTEM);③**系统管理页新增「登录页文案」区块**——大标题(≤64字)与底部小字(≤200字)两输入框+保存,留空即恢复默认;④**默认值仅前端持有**(`shared/config/loginBranding.ts`导出`DEFAULT_LOGIN_TITLE`「方案数据管理系统」/`DEFAULT_LOGIN_FOOTER`「© 方案数据管理系统 · 版权所有 · 严禁扩散」),未配置/读取失败均回落默认(初值即默认首屏不闪空);登录页每次挂载取最新值不缓存(管理员随时可改)。OpenAPI同步升级为`1.62.0`。 版本1.61,**角色新增删除仅超管、调阅审批人反查修复与审计筛选提示**:①**新增与删除角色仅超管**——`RoleService.guardRoleCreateDelete`在`POST /roles`与`DELETE /roles/{id}`入口把关,非内置超管一律403「新增与删除角色仅超级管理员可操作」(不限目标角色,1.60对管理员角色新建勾管理位/删除的细分拒绝被全覆盖),前端「新建角色」与列表行「删除」按钮对非超管置灰、悬停提示同文案;非超管角色管理员仅余编辑普通角色基本信息/权限矩阵与拖拽排序。②**调阅审批人反查修复**——`listConfigApprovers`原SQL join已停用的`sys_user.role_id`列(契约1.46起挂载读写全部改走`sys_user_role`关联表),导致关联表挂载的用户(如新配的方案管理员)收不到调阅待办、仅内置超管(服务层补充)能收到;改为按挂载真源`sys_user_role`解析生效角色(任一活跃挂载角色命中即通知),无任何挂载时回退默认角色(对齐`effectiveRole`),矩阵行限定`doc_kind='MAIN'`(契约1.48起调阅审批位仅主体方案行有效),密级×OWN/OTHER与用户密级秩过滤口径不变;通知面不含一级分类维度(偏宽),审批处理时运行时鉴权按文档精确判定兜底。③**日志审计筛选精简**——左侧「用户ID/组织ID/目标ID」三个按ID精确筛选的输入框移除(最终用户无从得知系统内部ID,筛选无从下手),关键字/动作/结果/对象类型/时间筛选保留;`GET /audit-logs`的`userId`/`organizationId`/`targetId`查询参数与`AuditLogQuery`能力保留(接口契约不动,仅撤UI入口)。接口签名零变更(OpenAPI不升版)。 版本1.60,**角色管理防提权限制**(除内置超管外,其他具备「角色管理」权限的用户受限,守卫统一由`RoleService.guardRoleAdminEdit`在`POST /roles`/`PUT /roles/{id}`/`PUT /roles/{id}/permissions`/`DELETE /roles/{id}`/`PUT /roles/{id}/users`五入口把关):①**管理员角色全禁**——管理权限矩阵中任一管理位勾选(`manageCategoryEdit`/`manageOrgEdit`/`manageUserEdit`/`manageSystemManage`/`manageAuditLog`/`manageRoleEdit`)的角色视为管理员角色,非超管不可修改其任何设置——`PUT /roles/{id}`与`PUT /roles/{id}/permissions`一律403「管理员角色仅超级管理员可修改」;`DELETE /roles/{id}`(删除即级联撤销其全部权限,破坏力强于修改)与`PUT /roles/{id}/users`(把管理员角色挂给他人/自己即自我提权)同样拒绝,前端该角色选中后基本信息、管理权限、权限矩阵统统置灰并提示「管理员角色仅超级管理员可修改,以下内容仅可查看」,列表行「编辑」按钮同样禁用;②**普通角色管理位不可变更**——管理位全空的角色非超管可编辑基本信息与保存权限矩阵,但六个管理位仅超管可设置:提交管理位与现值对比有任一变化即403「管理权限仅超级管理员可设置」(原值回传放行——基本信息编辑原样回传管理位不受影响),前端管理权限区块对非超管置灰、标题标注「(仅超级管理员可设置)」、保存按钮与复选框双重禁用(判定为`roleTypes.ts`共享导出`roleIsAdmin`,六管理位任一勾选,与后端`hasAnyManageFlag`同步维护;用户挂载角色不在此限制内——任何有用户管理权限者可给用户挂载任意角色);③**新建路径延伸堵住**——非超管`POST /roles`勾选任一管理位即403(新建界面不含管理位、恒默认全关,堵接口直调提权);④判定口径——超管=`roleCode=ADMIN`且`username=admin`(与附件权限收口1.43同口径`AuthorizationService.isSuperAdmin`),接口签名与载荷零变更(纯行为守卫,OpenAPI不升版)。 版本1.59,**附件矩阵库行组兜底与分类附件表隐藏**(`attachmentMatrixRows`改为库行组行(类型页签,`top_category_id IS NULL` × 类型 × 密级 × OWN)与挂载主案一级分类行**任一命中即放行**——库行组兜底对已挂载附件同样生效,查看/下载/编辑/删除四位一体;前端分类页签下附件矩阵表隐藏(`SHOW_CATEGORY_ATTACHMENT_MATRIX = false` 常量开关,改回 true 即恢复界面,后端判定恒支持分类行)、类型页签标题更「共享附件库(本类型全部附件的权限统一在此设置)」;迁移 V33(MySQL+达梦)将分类下附件行(`top_category_id IS NOT NULL AND doc_kind != 'MAIN'`)五位权限清零、行保留——附件权限统一收敛为类型页签单入口,分类行保留日后恢复配置的加细能力;新增已挂载附件库行组兜底用例;纯判定语义与呈现变更,接口形状无变化,OpenAPI 不升版) 版本1.58,**借阅统一更名调阅与矩阵列调阅审批**(用户可见文案全面「借阅」→「调阅」:调阅申请弹窗与调阅审批单详情弹窗(标题/表头「调阅人」/提交与错误提示)、调阅授权配置弹窗(标题/「选择调阅人」/表头/保存与重复提示)、后端错误消息(「无需调阅」「待审批的调阅申请」「调阅申请不存在」「该调阅申请已被处理」「调阅用户不存在、已删除或已禁用」「expiresAt调阅时限最长为7天」)与站内通知标题正文(「调阅审批申请」「申请调阅《…》」「调阅申请已通过/已拒绝」)同步;角色权限矩阵主案操作位列「配置权限」更名「调阅审批」(`canConfigPermission` 位,其现职责即调阅申请审批人);契约正文与 OpenAPI description/summary 中文术语同步(含错误消息字面量与章节名);URL(/borrow-requests)、字段名(borrowSummary 等)、审计动作(BORROW_REQUEST_*)、表名与迁移等代码标识符保留borrow 原名——纯文案变更,接口形状无变化,OpenAPI 不升版) 版本1.57,**高级检索密级修复与分段按钮组**(排查确认高级检索四维度中唯一缺陷在附件列表`GET /attachments`:单选`securityLevel`参数漏接线——该路仅`applyAdvancedFilters`内的多选`securityLevels`生效,前端下发的单选参数被忽略导致密级筛选不生效;修复为与方案列表`GET /documents`同口径,经`DocumentQueryService.SECURITY_LEVELS`校验后写入单数过滤键(常量改公开复用,多选参数保留兼容),非法值报400「securityLevel不是有效枚举值」;其余三维核验均正常——数据内容`keywordScope`两路一致、数据分类`categoryId`(方案)与`attachmentType`(附件)分派正确、提供单位`organizationId`子树过滤两路生效;前端高级检索四维度下拉改分段按钮组(对齐分类树字号「小/中/大」交互:相邻按钮拼接、首尾圆角、激活高亮、aria-pressed 状态),「全部」置首、选项密集自动换行;本次为缺陷修复与纯前端UI 呈现变更,接口形状与契约参数无变化,OpenAPI 维持 1.55.0 不升版) 版本1.56,**权限矩阵库页签按类型拆分**(纯前端交互重构,接口与矩阵行数据模型零变更——`GET/PUT /roles/{id}/permissions`行集与提交仍为期望键集全集,共享资料库行组仍为NULL×5密级×四类型×OWN共20行):①修复「共享资料库」页签无法激活——原页签以`id=null`标识、点击落入`undefined`(未选择态)且激活比对条件排除null页签导致永远回落第一个分类页签,页签键改为字符串(分类页签=分类id、类型页签=`library:{kind}`)后点击即激活;②库页签拆分——末位单一「共享资料库」页签展开为四个共享资料类型页签(配套方案/支撑成果/方案训考资料/典型案例,label取attachmentTypes字典),点击某类型页签只呈现该类型的附件矩阵表(库行组行,密级×该类型×OWN共5行、无主体方案表),不再对整库一次操作;类型页签附件表上方标题「共享附件库(未挂载附件权限在此处设置,已挂载附件在对应分类下设置)」区分两类设置位置;列级/密级级批量勾选的联动范围随页签限定(类型页签限定该类型行,密级rowSpan同步收缩为1行);③分类页签——主体方案表上方新增「主体方案」标题(原表无标题),共享资料表标题去掉括号提示(「共享资料(挂载到该分类主案下的资料按此判定;未挂载资料见「共享资料库」页签)」收敛为「共享资料」),仅主案模式提示与两张表布局不变。 版本1.55,**高级检索四维度重构**(①数据内容——方案列表`GET /documents`与附件列表`GET /attachments`新增可选参数`keywordScope`(`TITLE`/`CONTENT`/`ALL`),作用于`keyword`的匹配范围:`TITLE`=标题(缺省,兼容存量直调客户端)、`CONTENT`=正文(转`content_text` LIKE)、`ALL`=标题或正文任一命中(`document_name LIKE ... OR content_text LIKE ...`,同一下划线转义);`keyword`为空时该参数被忽略(匹配范围依附于关键词);聚合检索(`aggregateHits=true`)仅支持`TITLE`口径(命中统计按标题冒泡),传`CONTENT`/`ALL`返回400 INVALID_ARGUMENT「aggregateHits仅支持keywordScope为TITLE的标题范围检索」;正文命中片段——`keywordScope`为`CONTENT`/`ALL`且`keyword`非空时,列表摘要的正文补查与`contentSnippet`回落以`keyword`为片段关键词(`contentKeyword`参数仍优先),标题命中片段沿用`contentKeyword`原行为;②数据密级——前端高级检索由多选`securityLevels`改为单选`securityLevel`(方案/附件列表均支持,后端`securityLevels`多选参数保留兼容存量调用);③数据分类——复用现有`categoryId`(方案两路,前端选中一级分类按其含子分类子树过滤,即`includeDescendants=true`)与附件路`attachmentType`(四类型枚举),单选互斥;④提供单位——由模糊输入`organizationKeyword`改为单选机构下拉,`GET /documents`与`GET /attachments`支持`organizationId`(选中机构含其下级子树过滤,机构不存在或子树为空时返回空集),新增接口`GET /api/v1/organizations/filter-options`(任意登录用户):返回`{orgFilterLevel, organizations:[{id, orgName}]}`,选项为机构树第`orgFilterLevel`级启用机构(按`sort_no`升序null最后、同值按id),级别由新配置`DMS_SEARCH_ORG_FILTER_LEVEL`控制(缺省2,加入.env与两份.env.example),不做1.45部门子树裁剪(检索维度面向全机构);⑤前端——高级检索改为向下展开的四行单选面板(数据内容 全部/标题/正文、数据分类 全部+一级分类+四类共享资料、数据密级 全部+5级、提供单位 全部+第N级机构,选项挂载拉取失败静默仅保留「全部」),全局检索store字段收敛为`keywordScope`/`category`(`cat:{id}`/`att:{code}`前缀)/`securityLevel`/`organizationId`,过渡视图两路查询按页签分派(方案路`categoryId`+子树、附件路`attachmentType`),关键词非空时显式下发`keywordScope`(含`ALL`——UI「全部」须按标题或正文任一命中处理,后端缺省`TITLE`仅为兼容),旧字段`contentKeyword`/`securityLevels`(多选)/`organizationKeyword`不再由全局检索下发(后端参数保留)。OpenAPI同步升级为`1.55.0`。 版本1.54,**审批相关设置(调阅说明与调阅审批单模板)**(迁移`V32__borrow_settings`(Flyway,含达梦适配版)/`0045_borrow_settings`(Alembic),新建通用键值表`sys_setting`:`setting_key` VARCHAR(64)主键、`setting_value` MEDIUMTEXT(达梦CLOB)NULL、`file_name` VARCHAR(255)、`file_size` BIGINT、`updated_at`、`updated_by`,行不存在=未设置):①两个设置项——「调阅说明」存`borrow.notice`行(≤4000字,随调阅对话框展示)、「调阅审批单模板」PDF存`borrow.template`行元数据(原始文件名与字节数),文件落盘存储根`settings/borrow-template.pdf`固定路径(上传流式计量大小、魔数校验、临时文件+原子替换,覆盖式;清除时删行并删文件);②接口——`GET /api/v1/borrow-settings`任意登录用户可读(调阅对话框与系统管理页共用),响应`{notice,templateFileName}`,null=未设置;`PUT /api/v1/borrow-settings`保存说明(`manage_system`位,请求体必含且仅含`notice`,传null/空白即清空,>4000字返回400「调阅说明不能超过4000字」);`POST /api/v1/borrow-settings/template`上传模板(`manage_system`位,multipart`file`必须,仅PDF(415)且≤`DMS_MAX_FILE_SIZE`(413),响应`{templateFileName}`);`DELETE /api/v1/borrow-settings/template`清除模板(`manage_system`位);`GET /api/v1/borrow-settings/template`下载模板(任意登录用户,PDF附件直出,未上传404「调阅审批单模板未上传」);③审计——写操作(保存说明/上传/清除模板)记`BORROW_SETTINGS_UPDATE`(detail kind:notice/template-upload/template-delete,targetType=SYSTEM),DmsEnums与共享字典auditActions同步;④upsert两端通用——先UPDATE、影响0行再INSERT(MySQL与达梦无方言分支);⑤前端——系统管理页新增「审批设置」区块(调阅说明多行文本+保存、模板上传/替换/清除带危险确认),调阅对话框挂载即拉取设置(失败静默不阻断申请),有说明时顶部展示说明块(保留换行、限高滚动),有模板时「审批单」行尾提供「下载模板」。OpenAPI同步升级为`1.54.0`。 版本1.53,**密码即角色(挂载角色独立密码)**(迁移`V31__role_password`(Flyway,含达梦适配版)/`0044_role_password`(Alembic),为`sys_user_role`加`password_hash` VARCHAR(255) NULL列):①核心模型——N个用户级密码与N个挂载角色一一对应,登录**输入哪个角色的密码即直接以该角色进入**(无需显式选择角色);挂载行`password_hash` NULL=该角色使用用户级密码;②分配硬约束——挂载≥2角色时必须**恰一个角色**「使用用户密码」(其余设独立密码),独立密码两两互异且不得等于用户级密码(创建/门户设密比对明文、编辑用旧哈希verify),违反返回400 `PASSWORD_CONFLICT`「各角色密码不能相同,且不能与用户密码相同」;恰一个NULL消除登录匹配歧义;③登录匹配顺序——`POST /auth/login`先遍历挂载行非空哈希,命中即以该角色签发进入;未命中回退用户级密码验证(锁定计数仅密码全不匹配时触发);用户级密码命中且恰一个NULL挂载行→以该角色进入;挂载行全NULL多角色=存量过渡态,维持1.46角色选择步骤(不签发),显式`roleId`二轮制路径不变;④用户创建/编辑——请求体新增可选`rolePasswords`数组`[{roleId,password}]`(password为null/缺省=使用用户密码):创建挂≥2必填,挂载≤1提交返回400「仅多角色需要分配角色密码」;编辑**仅挂载变化时受理**(结果≥2全量重新分配、≤1清空全部挂载行密码回用户级密码),挂载未变提交返回400「挂载角色未变化,无需重新分配角色密码」;校验含恰一个NULL(「必须为一个角色选择使用用户密码」)、覆盖全部挂载角色、无重复、roleId须指向本次挂载集、密码策略同用户密码;⑤门户设密——`POST /auth/portal/verify` SET_PASSWORD阶段响应多角色时新增`roles`挂载简表,`POST /auth/portal/setup-password`新增可选`rolePasswords`(多角色首登逐角色分配,形态同④),设密后以「使用用户密码」的角色直接签发进入;⑥管理员重置密码与1.49自助重置审批通过——置空/重设用户级密码的同时**清空全部挂载行密码**(回过渡态:多角色用户下次设密时逐角色重新分配);⑦前端——用户表单挂载角色列表行内融合分配UI(勾选≥2时每勾选行尾radio「用户密码」同名互斥默认第一个勾选角色、未选中行内嵌独立密码+确认输入框带小眼睛,编辑未动挂载不出密码区,列表上方一句「登录时输入哪个角色的密码即进入该角色」),门户设密界面多角色同形态分配,提交前预校验(策略/两次一致/互异/不等于用户密码)。OpenAPI同步升级为`1.53.0`。 版本1.52,**席位输入提示**(用户表单席位输入框placeholder由「选填,展示于右上角用户信息」改为「请输入席位」,仅前端文案变更,接口与校验不变) 版本1.51,**用户名即姓名(姓名概念全面退出界面)**(①用户列表筛选「按用户名或姓名筛选」、共享资料「搜索用户名或姓名」、借阅/授权人员选择「姓名或用户名」统一收敛为「用户名」;②顶栏用户信息面板「姓名:」更名「用户名:」,审计日志页列头「用户名 / 姓名」改「用户名」(内容展示realName、存量行缺姓名回退账号不变);③借阅申请人员列表不再附显英文账号「(username)」,单点登录独立密码界面身份回显框由username改显realName,重置密码待办通知文案去掉「(username)」;④新建用户界面录入的即姓名——username与realName同值写入两列(后端username仅校验非空≤64无字符集白名单,中文姓名直接可用,创建接口不变);⑤存量用户英文username保留在库中作登录兼容(登录/重置密码按用户名精确优先+姓名唯一命中回退逻辑不变),仅不再界面显示) 版本1.50,**姓名概念退出界面**(登录页与重置密码弹窗的提示由「请输入用户名或姓名」收敛为「请输入用户名」(弹窗用户名标签同步去掉「或姓名」;后端登录/重置密码仍兼容按姓名唯一命中解析存量用户,接口不变);用户维护列表「姓名」列更名为「用户名」且单元格展示realName(显示名),不再单独展示姓名列;新建/编辑用户表单移除「姓名」项——新建时realName由用户名同步写入(创建接口realName仍必填、应用层唯一校验不变),编辑提交保持原值,存量用户真实姓名不被覆盖) 版本1.49,**登录界面重置密码审批流**(迁移`V30__password_reset_request`(Flyway,含达梦适配版)/`0043_password_reset_request`(Alembic)):①申请接口——新增公开接口`POST /auth/reset-password-requests`(登录前可调,`security: []`),请求体`username`(接受用户名或姓名,重名歧义按用户不存在处理,与登录1.47口径一致)+ 验证码开启时`captchaId`/`captchaCode`(与登录同一验证码、一次性消费);内置admin拒绝申请(400)、停用用户拒绝(400)、已有待审申请返回409 `PASSWORD_RESET_DUPLICATE`;②审批角色路由——按申请用户类型路由:首长(userType=COMMANDER)→`DMS_RESET_PASSWORD_APPROVER_ROLE_COMMANDER`(缺省「安全管理员」)、其他用户→`DMS_RESET_PASSWORD_APPROVER_ROLE_NORMAL`(缺省「系统管理员」),两键加入.env/.env.example(值为角色名,角色须存在且启用、至少一名活跃挂载用户,否则申请400);向该角色全部活跃挂载用户发`PASSWORD_RESET`类型待办通知(bizId指向申请单);③审批——审批人在通知页待办同意/拒绝(复用`POST /notifications/{id}/handle`,二次认证、先到先得、PENDING行锁+CAS守卫,后到者409),审批人实时鉴权(内置超管或当前挂载申请单所路由的审批角色,否则403);其余审批人待办同步失效、申请人收结果通知;④审批通过效果——该用户`password_hash`与`last_login_at`置NULL(回首次登录态:门户验签据此进入`SET_PASSWORD`阶段重新设置独立密码)、`auth_version`+1(当前全部会话立即失效)、会话代/锁定痕迹清零;本地账号密码登录对已重置账号返回403「该账号密码已重置,请通过单点登录进入并重新设置密码」;⑤首长重置收口——`PUT /users/{id}/reset-password`对userType=COMMANDER返回403「首长的密码不支持此方式重置,请本人通过登录页的重置密码申请办理」,前端用户管理首长行隐藏「重置密码」按钮;⑥通知类型扩展——`NotificationSummary.notifyType`枚举新增`PASSWORD_RESET`(bizId指向重置密码申请单),借阅APPROVAL待办的「查看」详情按钮仅对APPROVAL类型渲染;⑦审计——新增动作`PASSWORD_RESET_REQUEST_CREATE`(公开接口authAudit)/`PASSWORD_RESET_APPROVE`/`PASSWORD_RESET_REJECT`(businessAudit,targetType=PASSWORD_RESET_REQUEST);⑧前端——账号密码登录页与单点登录独立密码界面均提供「重置密码」入口,弹窗内输入用户名/姓名(+验证码)提交,成功提示等待审批与通过后重新设密;首次设密阶段(SET_PASSWORD)不提供入口。OpenAPI同步升级为`1.49.0`。 版本1.48,**方案权限矩阵三维化**(迁移`V29__matrix_top_category_kind`(Flyway,含达梦适配版)/`0042_matrix_top_category_kind`(Alembic)):①行键三维化——`sys_role_permission`行键由「密级 × 部门范围」扩为「一级分类 × 种类 × 密级 × 部门范围」,新增`top_category_id` BIGINT NULL(未删除一级分类id;**NULL=共享资料库行组**,未挂载附件判定专用)与`doc_kind` VARCHAR(32) NOT NULL DEFAULT 'MAIN'(`MAIN`=主体方案;`ACCOMPANYING_PLAN`/`SUPPORT_ACHIEVEMENT`/`TRAINING_MATERIAL`/`TYPICAL_CASE`=共享资料四类型,恒OWN行),存量按一级分类同权展开(实际权限升级前后不变),仅主案模式(flat_mode=1)的一级分类不生成共享资料行;②矩阵接口载荷——`GET /roles/{id}/permissions`与`PUT /roles/{id}/permissions`行结构新增必返必填`topCategoryId`(数字串或null)与`docKind`,矩阵行数动态=期望键集:每个未删一级分类 × 5密级 × (MAIN的OWN+OTHER行 + 非仅主案分类的附件四类型OWN行) + 共享资料库行组(NULL × 5密级 × 四类型 × OWN)(常规分类30行/个、仅主案分类10行/个、共享资料库恒20行),提交仍为期望键集全集(行数或键组合不符返回400「权限矩阵必须为N行」/「矩阵行的分类、种类、密级或部门范围组合无效」);读时归一化——分类新增、仅主案模式关闭或开关切换后缺失行自动按全0空行补齐输出、已删分类残留行不输出;保存仅拒绝库内重复脏行(同一矩阵键两行返回409「角色权限矩阵数据不完整,请联系管理员核查」),缺失行diff有变化时按提交值自动补插、残留行自然冷落;交叉位守卫——主体方案行不可设置附件5位(400「主体方案行不可设置附件权限位」)、附件行与共享资料库行不可设置主体方案8位(400「共享资料行不可设置主体方案权限位」),OTHER行canUpload仍拒真(400「上传权限仅可设置本部门方案」);③判定口径——主体方案按文档所属分类沿父链上溯顶级分类取MAIN行(canUploadPlan新增categoryId入参,OWN/OTHER部门判定保留);已挂载附件按其挂载主案的顶级分类 × 附件类型 × 附件密级 × OWN行判定(多主案任一命中即放行);未挂载附件走共享资料库行组(topCategoryId=NULL)判定,上传附件canUploadAttachment按附件类型与密级取共享库OWN行;停用(未删除)分类下的存量文档仍按其行判定;新增一级分类下次读取时自动补0行(安全缺省);④「扁平模式」更名「仅主案模式」——仅UI名称与提示文案变更(表单提示改为「勾选后该分类下的主方案不挂载配套方案、支撑成果、方案训考资料、典型案例等共享资料(仅一级分类可设置)」),字段名`flatMode`/`flat_mode`与接口结构不变;开启冲突409 `CATEGORY_FLAT_MODE_CONFLICT`文案同步为「该一级分类下已存在共享资料挂载,请先解绑后再开启仅主案模式」;⑤前端角色维护——方案权限矩阵按一级分类页签分组(分类在前、密级在后,末位固定「共享资料库」页签),每页签呈现「主体方案」(密级 × 部门范围,主案8位)与「共享资料」(密级 × 资料类型,附件5位)两张表,仅主案模式页签不渲染共享资料表。OpenAPI同步升级为`1.48.0`。 版本1.47,**用户名与姓名同语义**(迁移`V28__admin_real_name`(Flyway,含达梦适配版)/`0041_admin_real_name`(Alembic)):①登录凭证扩展——`POST /auth/login` 的 `username` 字段同时接受用户名或姓名:先按用户名精确匹配,未命中时按姓名匹配活跃用户,**仅唯一命中放行**;重名歧义(多个活跃用户同名)一律按 `401 INVALID_CREDENTIALS` 出口,与其他凭证错误不可区分(不泄露重名情况);账号命中后不再触发姓名回退;②姓名唯一性(应用层校验,**不加数据库唯一约束**)——`POST /users` 与 `PUT /users/{id}` 校验 `realName` 在活跃用户中唯一(编辑排除自身),冲突返回 `409 DATA_VERSION_CONFLICT`("姓名已存在");存量重名不在本次迁移范围(应用层校验只约束增量);③数据订正——内置 `admin` 的 `real_name` 由「系统管理员」迁移为「超级管理员」(避免与常见姓名冲突);④强制下线响应 `data` 新增 `realName` 字段(与 `username` 并列,前端提示展示姓名);⑤前端配套(不入契约):显示用户名处一律直接显示姓名(用户列表删「用户名」列、审计操作人列、水印、删除人快照、借阅选人等),右上角用户信息面板只保留姓名/单位/席位三项,登录框占位符改「请输入用户名或姓名」。OpenAPI同步升级为`1.47.0`。 版本1.46,**多角色架构与席位/用户类型**(迁移`V27__user_role_mount`(Flyway,含达梦适配版)/`0040_user_role_mount`(Alembic)):①`sys_user_role`多对多挂载表上线,一个用户可挂载多个功能角色,`sys_user.role_id`/`role_name`单值列停用不删(存量数据迁移进挂载表);②登录两轮制——`POST /auth/login`新增可选`roleId`(数字字符串),密码/验证码/锁定/IP全过后挂载多角色且未带`roleId`返回选择步骤`{roleSelectionRequired, username, realName, roles:[{id,roleName}]}`(不签发不记成功),前端选定后携`roleId`二次提交(密码与验证码重新校验),单角色自动选中、无挂载走默认角色,会话锁定单角色(JWT新增`roleId` claim,切换角色需重新登录);SSO非静默登录(输入密码走`/auth/login`)同流程;门户首次设密完成直接签发(`roleId` claim为`null`走默认角色,属安全收敛方向);③`UserSummary`新增`roles`(全部活跃启用挂载简表)、`seatName`(席位,≤64字符可空)、`userType`(`NORMAL`/`COMMANDER`=首长,仍属USER类别,重置密码差异化后续实现),`roleId`/`roleName`展示口径改为本人=会话角色、列表行=第一挂载角色,`allowedModules`按会话选定角色计算;④用户创建/编辑请求体`roleId`改`roleIds`(ID数组,未知字段严格拒绝),新增`seatName`/`userType`——编辑以**活跃启用挂载为基线**做差集增删(停用角色关联保留不动,重新启用后自动恢复生效),diff变化联动`auth_version+1`使旧Token(含会话角色claim)失效;`PUT /users/{id}`二次认证触发条件同步为`roleIds`集合变化;⑤角色成员接口(`roles/{id}/users`)改为操作挂载表关联(只增删本角色关联,成员变动同样联动对端用户`auth_version+1`),角色删除前置守卫「有活跃启用挂载用户时409」;挂载变化防提权校验逐角色遍历(操作者按会话角色解析管理范围);⑥前端登录页(账号密码与SSO输密码)增加角色选择框(默认选中第一项、验证码开启时第二轮重新输入),用户表单角色改多选、新增席位输入与「首长」用户类型,右上角用户信息第二行展示「机构+空格+席位」。OpenAPI同步升级为`1.46.0`。 版本1.45,**用户与机构只读接口放开**(修复:普通用户借阅申请添加借阅人报403「无权限执行此操作」——两个只读接口按ADMIN类别拦截):①`GET /api/v1/users`与`GET /api/v1/organizations/tree`不再要求ADMIN类别,任意登录用户可调(GET只读不修改内容);②范围口径——普通用户(USER)调用户列表不传机构时静默收敛到本人所在部门子树(含下级按`includeDescendants`展开、沿ENABLED机构边、无部门空页),显式请求机构不在本人部门子树返回403「无权限查看该机构的用户」(与锚点管理员同口径);机构树恒返回本人部门为根的子树(森林形式、无部门空树);ADMIN类别维持既有管理范围收敛不变;③返回DTO不变(`UserSummary`全量字段与机构树结构),普通用户可见范围为本人部门子树内的数据;④前端借阅申请弹窗「添加借阅人」直接复用上述两个通用接口(机构下拉对普通用户展示本部门子树)。OpenAPI同步升级为`1.45.0`。 版本1.44,**附件删除权限位**:①方案权限矩阵新增**附件删除位**——`RolePermissionRow`布尔位扩为13个(新增`canAttachmentDelete`),仅OWN行可设置(OTHER行提交true返回`400 INVALID_ARGUMENT`「附件权限仅可设置本部门行」,与其余四个附件位同口径);迁移`V26__attachment_delete_permission`(Flyway,含达梦适配版)为`sys_role_permission`加`can_attachment_delete`列(存量行回填false);②**附件删除不再按ADMIN类别专属**——`DELETE /api/v1/attachments/{id}`与`requirePlanAction`附件DELETE分支改为仅内置超管恒可,受限ADMIN与USER同按附件密级取OWN行`can_attachment_delete`判定(1.43的「删除维持ADMIN类别专属」前提「矩阵无删除位」不再成立,删除收编入附件位体系);普通用户/受限ADMIN的`allowedActions`按位下发`DELETE`(原受限ADMIN恒下发DELETE位的规则废止);③`permAttachmentLibrary`会话位的`countOwnAnyAttachment`计数纳入删除位(仅有删除位的角色也可见共享资料库页签);④前端角色维护界面矩阵新增「附件删除」列(仅本部门行可选,跨行合并单元格),删除按钮按`allowedActions`的`DELETE`位显隐自动适配。无新增接口路径。OpenAPI同步升级为`1.44.0`。 版本1.40,**验证码缺省关闭**:①`DMS_CAPTCHA_ENABLED` 缺省值由 `true` 改为 `false`(键名与接受值 1/true/yes/on 不变)——未显式配置的部署升级后登录页不再显示图片验证码、登录请求回归三键(username/password/keepSignedIn),显式设 `true` 的部署不受影响;②前端无需变更(本就按 `GET /auth/captcha` 的 `enabled` 决定验证码输入显隐);③`.env` 与两份 `.env.example` 显式落 `DMS_CAPTCHA_ENABLED=false` 便于运维感知;④同批前端体验调整(不入契约):双击列表项与「查看」按钮打开的查看弹窗默认全屏(`FilePreviewModal` 新增 `initialWindowState` 入参);左侧分类树一级分类竖向页签未选中态反色为主题色底白字(选中态维持白底主题色字)。无数据库结构变更、无新增接口路径。OpenAPI 同步升级为 `1.40.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`。