DMS_API_CONTRACT.md 36 KB

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

契约版本:1.5 状态:已确认,统一方案上传与同名覆盖接口基线 外部字段风格:camelCase
数据库字段风格:snake_case

1. 使用规则

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

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

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

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

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

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

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

2. 通用约定

2.1 基础路径与认证

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

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

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

X-Request-Id: <requestId>

2.2 标识符与时间

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

2.3 JSON成功响应

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

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

2.4 分页数据

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

默认值:

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

2.5 错误响应

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

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

2.6 HTTP状态

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

2.7 并发更新

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

HTTP 409
code = DATA_VERSION_CONFLICT

成功修改后返回新的 rowVersion

2.8 Blob响应

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

PDF预览:

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

下载:

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

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

3. 固定枚举

3.1 角色

USER
ADMIN

3.2 模块

DOCUMENT_BROWSER
BACKEND_MANAGEMENT
AUDIT_LOG

3.3 文档类型

MAIN
SUB_PLAN
ATTACHMENT

3.4 文档状态

DRAFT
PUBLISHED
ARCHIVED

3.5 密级

PUBLIC
INTERNAL
SECRET
CONFIDENTIAL
TOP_SECRET

3.6 可见范围

ALL_AUTHENTICATED
ORGANIZATION
CUSTOM

3.7 附件类型

POLICY
WORK_STANDARD
TABLE
DIAGRAM
OTHER

3.8 权限主体

ORG
USER

3.9 可执行操作

VIEW
DOWNLOAD
EDIT
CONFIG_PERMISSION
DELETE
BIND_ATTACHMENT
UNBIND_ATTACHMENT
RESTORE_DOCUMENT

3.10 分类类型

SCENE
STYLE
SITUATION
VERSION
OTHER

3.11 启停状态

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

ENABLED
DISABLED

3.12 审计结果

SUCCESS
FAILURE

3.13 审计动作

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

3.14 审计目标

AUTH
CATEGORY
DOCUMENT
ATTACHMENT
PERMISSION
ATTACHMENT_BINDING

4. 公共DTO

4.1 UserSummary

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

第一阶段模块映射固定为:

  • USERDOCUMENT_BROWSER
  • ADMINDOCUMENT_BROWSERBACKEND_MANAGEMENTAUDIT_LOG

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

4.2 CategoryNode

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

4.3 DocumentSummary

{
  "id": "1001",
  "documentName": "2024年度综合应急预案",
  "summary": "方案内容概述",
  "documentType": "MAIN",
  "status": "PUBLISHED",
  "securityLevel": "SECRET",
  "visibilityType": "ORGANIZATION",
  "visibilitySummary": "作战部",
  "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/rootDocumentIdnull
  • visibilityType 固定 ALL_AUTHENTICATED
  • securityLevel 固定 PUBLIC
  • attachmentType 必填。

4.4 DocumentDetail

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

{
  "originalFileName": "2024年度综合应急预案.docx",
  "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "fileSize": 204800,
  "fileHash": "SHA-256十六进制字符串",
  "permissionSummary": {
    "organizationCount": 2,
    "userCount": 3,
    "inheritedFromMainPlan": false
  }
}

4.5 RecycleBinDocumentSummary

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

categoryIdcategoryNameparentDocumentIdupdatedByupdatedByNamedeletedBy允许为 nulldeletedBy存在时,其四个字段也分别允许为 null

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

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

4.6 RestoreDocumentResult

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

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

5. 健康检查

GET /api/v1/health

无需认证。

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

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

6. 认证接口

6.1 POST /api/v1/auth/login

无需认证。

请求:

{
  "username": "admin",
  "password": "password",
  "keepSignedIn": true
}

成功:

{
  "code": "OK",
  "message": "success",
  "data": {
    "accessToken": "jwt",
    "tokenType": "Bearer",
    "expiresIn": 7200,
    "user": {}
  },
  "requestId": "uuid"
}

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

Token有效期固定为:

  • keepSignedIn=falseexpiresIn=7200
  • keepSignedIn=trueexpiresIn=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:可选。

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

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

7.2 GET /api/v1/users

查询参数:

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

返回 PageResult<UserSummary>

8. 分类接口

8.1 GET /api/v1/categories/tree

查询参数:

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

返回 CategoryNode[]

8.2 POST /api/v1/categories

管理员接口。

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

返回新建 CategoryNode,HTTP 201

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

管理员接口。

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

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

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

查询参数:

rowVersion=<integer>

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

9. 主案与子方案

9.1 GET /api/v1/documents

查询参数:

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

页签调用规则:

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

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

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

返回 PageResult<DocumentSummary>

9.2 POST /api/v1/documents

管理员接口,multipart/form-data

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

主案 metadata:

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

子方案 metadata 增加:

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

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

名称唯一性与覆盖:

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

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

管理员接口,multipart/form-data

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

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

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

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

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

仅用于 MAINSUB_PLAN,返回 DocumentDetail

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

管理员接口,只编辑元数据,不替换文件。

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

不允许修改 documentTypeparentDocumentIdrootDocumentId 和文件身份字段。

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

查询参数:

rowVersion=<integer>

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

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

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

查询参数:

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

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

10. 共享附件

10.1 GET /api/v1/attachments

查询参数:

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

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

10.2 POST /api/v1/attachments

管理员接口,multipart/form-data

  • file
  • metadata

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

后端强制:

documentType=ATTACHMENT
securityLevel=PUBLIC
visibilityType=ALL_AUTHENTICATED
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}

管理员接口:

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

不得修改附件固定密级、固定可见范围和文件身份。

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

查询参数:

rowVersion=<integer>

存在有效挂载时:

HTTP 409
code = ATTACHMENT_IN_USE
details.mountedPlanCount
details.mainPlans

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

返回:

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

11. 附件挂载

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

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

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

每项使用附件 DocumentSummary,增加:

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

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

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

管理员接口:

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

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

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

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

查询参数:

mainPlanRowVersion=<integer>

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

12. 权限

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

主案返回:

{
  "documentId": "1001",
  "sourceDocumentId": "1001",
  "inherited": false,
  "visibilityType": "CUSTOM",
  "documentRowVersion": 3,
  "entries": [
    {
      "id": "7001",
      "subjectType": "ORG",
      "subjectId": "10",
      "subjectName": "作战部",
      "canView": true,
      "canDownload": true,
      "canEdit": false,
      "canManagePermission": false,
      "canDelete": false
    }
  ]
}

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

  • sourceDocumentId 为主案ID;
  • inherited=true

共享附件调用返回 400 ATTACHMENT_HAS_NO_ACL

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

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

{
  "visibilityType": "CUSTOM",
  "documentRowVersion": 3,
  "entries": [
    {
      "subjectType": "ORG",
      "subjectId": "10",
      "canView": true,
      "canDownload": true,
      "canEdit": false,
      "canManagePermission": false,
      "canDelete": false
    }
  ]
}

规则:

  • 请求是权限全集,不是增量补丁;
  • ALL_AUTHENTICATEDentries 必须为空;
  • ORGANIZATION 时只允许 ORG
  • CUSTOM 允许 ORGUSER
  • 重复主体返回 400 DUPLICATE_PERMISSION_SUBJECT
  • 任一动作布尔值均不允许省略;
  • 后端在一个事务中完成差异更新、版本递增和审计。

13. 文件读取

以下两个接口适用于 MAINSUB_PLANATTACHMENT,不再建立附件专用文件路径。

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

  • PDF成功返回 inline Blob;
  • DOCX成功返回原始DOCX二进制和正式MIME;DOC、XLS、XLSX返回 415 PREVIEW_UNAVAILABLE
  • 无查看权限返回 403 DOCUMENT_VIEW_FORBIDDEN
  • 文件缺失返回 404 FILE_NOT_FOUND

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

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

14. 回收站与文档恢复

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

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

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

查询参数:

  • keyword
  • documentTypeMAIN|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
  • sortDirectionasc|desc,默认 desc

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

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

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

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

请求体必须且只能包含:

{
  "rowVersion": 3
}

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

通用恢复校验:

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

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

MAIN恢复采用方案B:

  • 恢复主案本身;
  • 只恢复与本次删除具有相同文档ID、deletedAtupdatedBy的级联删除ACL和挂载关系;
  • 不恢复更早历史删除关系;
  • 权限主体有效时恢复并刷新主体名称快照,主体删除或禁用时跳过并计数;
  • 挂载附件有效时恢复,附件删除或不存在时跳过并计数;
  • 已存在同主体有效ACL或同附件有效挂载时返回关系冲突并整体回滚;
  • ORGANIZATION最终必须至少存在一个有效且 canView=true 的ORG权限;
  • CUSTOM允许空权限集合,普通用户默认无权访问;
  • ALL_AUTHENTICATED最终权限集合必须为空;
  • 重新统计分类 documentCount、主案 childCountattachmentCount
  • allowedActions由现有授权服务动态计算。

SUB_PLAN恢复:

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

ATTACHMENT恢复后强制保持:

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

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

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

版本规则:

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

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

固定锁顺序:

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

14.3 B8迁移要求

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

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

15. 审计和统计

15.1 GET /api/v1/audit/logs

查询参数:

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

返回分页日志:

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

15.2 GET /api/v1/statistics/documents

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

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

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

参数:createdFromcreatedTogranularity=DAY|WEEK|MONTH

返回:

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

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

参数:createdFromcreatedTo

返回动作分布:

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

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

参数:createdFromcreatedTolimitlimit 默认10、最大100。

返回活跃用户:

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

16. 标准错误码

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

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
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_PERMISSION_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 原方案分类无效,无法恢复 categoryIdreason=NOT_FOUND\|DELETED\|DISABLED
RESTORE_PARENT_INVALID 409 原所属主案无效,无法恢复 parentDocumentIdreason=NOT_FOUND\|DELETED\|WRONG_TYPE\|ROOT_MISMATCH
RESTORE_PERMISSION_INVALID 409 恢复后无法形成有效权限配置 invalidCountreason=NO_VALID_ORGANIZATION_PERMISSION\|ALL_AUTHENTICATED_HAS_PERMISSIONS
RESTORE_RELATION_CONFLICT 409 历史权限或挂载关系与当前有效关系冲突 permissionConflictCountbindingConflictCount

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

17. OpenAPI同步要求

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

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

17A. 统一UI字典

17A.1 查询接口

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

DictionaryOption

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

UiDictionaries

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

dictionaries必须固定包含以下14个数组:roleCodesallowedModulesdocumentTypesdocumentStatusessecurityLevelsvisibilityTypesattachmentTypessubjectTypesallowedActionscategoryTypesenabledStatusesauditResultsauditActionsauditTargets

每个数组按 sortNocode稳定排序。外部字段使用 camelCasesortNo为整数,codelabel为字符串。响应不得包含配置路径、加载时间或内部异常;响应体 requestIdX-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或后续版本。

20. Q3-B 1.4接口补充

  1. 正式RoleCode仅为ADMINUSER。四个审计查询接口与文档统计接口均要求ADMIN。历史AUDITOR不能登录,旧Token必须因authVersion或用户状态校验返回401。
  2. GET /api/v1/documents新增可选布尔参数includeDescendants。非法布尔值返回400 INVALID_ARGUMENT。仅在传入categoryId时true表示包含该分类全部有效后代;缺省或false为精确分类;未传分类时查询当前用户全部可见MAIN。
  3. MAIN创建metadata仅接受documentNamedocumentType=MAINcategoryIdsecurityLevelsummarytags。SUB_PLAN创建仅接受documentNamedocumentType=SUB_PLANparentDocumentIdsecurityLevelsummarytags。未知字段严格拒绝。单文件和批量导入一致。
  4. MAIN编辑仅接受documentNamesummarycategoryIdsecurityLeveltagsrowVersion;SUB_PLAN编辑不含categoryId。两者均不接受status或visibilityType。
  5. 非叶子分类用于MAIN创建或编辑时返回409 CATEGORY_NOT_LEAF,details至少包含字符串categoryId和整数childCategoryCount
  6. GET /api/v1/attachments仅ADMIN可访问。附件详情、统一preview/download路径对USER执行“至少一个有效挂载主案及对应VIEW/DOWNLOAD权限”检查。
  7. GET /api/v1/documents/{id}/preview的200响应支持application/pdfapplication/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.version1.4.0,迁移为0003_q3_business_alignment_and_content_search