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

> 契约版本：1.6
> 状态：已确认，LibreOffice 离线文档转换与 PDF 预览接口基线
> 外部字段风格：`camelCase`  
> 数据库字段风格：`snake_case`

## 1. 使用规则

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

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

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

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

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

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

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

## 2. 通用约定

### 2.1 基础路径与认证

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

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

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

```text
X-Request-Id: <requestId>
```

### 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 权限主体

```text
ORG
USER
```

### 3.9 可执行操作

```text
VIEW
DOWNLOAD
EDIT
CONFIG_PERMISSION
DELETE
BIND_ATTACHMENT
UNBIND_ATTACHMENT
RESTORE_DOCUMENT
```

### 3.10 分类类型

```text
SCENE
STYLE
SITUATION
VERSION
OTHER
```

### 3.11 启停状态

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

```text
ENABLED
DISABLED
```

### 3.12 审计结果

```text
SUCCESS
FAILURE
```

### 3.13 审计动作

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

### 3.14 审计目标

```text
AUTH
CATEGORY
DOCUMENT
ATTACHMENT
PERMISSION
ATTACHMENT_BINDING
```

## 4. 公共DTO

### 4.1 UserSummary

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

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

- `USER`：`DOCUMENT_BROWSER`
- `ADMIN`：`DOCUMENT_BROWSER`、`BACKEND_MANAGEMENT`、`AUDIT_LOG`

前端必须使用后端返回的 `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",
  "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/rootDocumentId` 为 `null`；
- `visibilityType` 固定 `ALL_AUTHENTICATED`；
- `securityLevel` 固定 `PUBLIC`；
- `attachmentType` 必填。

### 4.4 DocumentDetail

`DocumentDetail` 包含 `DocumentSummary` 的全部字段，并增加：

```json
{
  "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

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

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

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

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

### 4.6 RestoreDocumentResult

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

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

## 5. 健康检查

### `GET /api/v1/health`

无需认证。

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

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

## 6. 认证接口

### 6.1 `POST /api/v1/auth/login`

无需认证。

请求：

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

成功：

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

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

Token有效期固定为：

- `keepSignedIn=false`：`expiresIn=7200`；
- `keepSignedIn=true`：`expiresIn=604800`。

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

### 6.2 `POST /api/v1/auth/logout`

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

### 6.3 `GET /api/v1/users/me`

返回当前 `UserSummary`。

## 7. 组织和人员

### 7.1 `GET /api/v1/organizations/tree`

查询参数：

- `keyword`：可选，组织名称或编码；
- `status`：可选。

返回组织树数组，每个节点至少包含：

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

### 7.2 `GET /api/v1/users`

查询参数：

- `organizationId`
- `includeDescendants`，默认 `false`
- `keyword`
- `status`
- `page`
- `pageSize`

返回 `PageResult<UserSummary>`。

## 8. 分类接口

### 8.1 `GET /api/v1/categories/tree`

查询参数：

- `keyword`：只搜索分类名称或编码；
- `status`：可选。

返回 `CategoryNode[]`。

### 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=<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`
- `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<DocumentSummary>`。

### 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}`

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

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

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

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

查询参数：

```text
rowVersion=<integer>
```

主案存在有效子方案时返回 `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<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`

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

后端强制：

```text
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}`

管理员接口：

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

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

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

查询参数：

```text
rowVersion=<integer>
```

存在有效挂载时：

```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`

管理员接口：

```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}`

查询参数：

```text
mainPlanRowVersion=<integer>
```

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

## 12. 权限

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

主案返回：

```json
{
  "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`。

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

规则：

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

## 13. 文件读取

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

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

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

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

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

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

## 14. 回收站与文档恢复

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

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

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

查询参数：

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

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

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

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

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

请求体必须且只能包含：

```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或同附件有效挂载时返回关系冲突并整体回滚；
- `ORGANIZATION`最终必须至少存在一个有效且 `canView=true` 的ORG权限；
- `CUSTOM`允许空权限集合，普通用户默认无权访问；
- `ALL_AUTHENTICATED`最终权限集合必须为空；
- 重新统计分类 `documentCount`、主案 `childCount`和`attachmentCount`；
- `allowedActions`由现有授权服务动态计算。

SUB_PLAN恢复：

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

ATTACHMENT恢复后强制保持：

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

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

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

版本规则：

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

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

固定锁顺序：

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

### 14.3 B8迁移要求

B8实现必须创建 `0002_add_restore_audit_action`，不得修改 `0001_initial_schema`。迁移必须：

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

## 15. 审计和统计

### 15.1 `GET /api/v1/audit/logs`

查询参数：

- `keyword`
- `createdFrom`
- `createdTo`
- `userId`
- `organizationId`
- `actionType`
- `operationResult`
- `targetType`
- `targetId`
- `page`
- `pageSize`
- `sortDirection`，默认 `desc`

返回分页日志：

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

### 15.2 `GET /api/v1/statistics/documents`

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

`planDocumentCount = mainPlanCount + subPlanCount`，不包含附件。

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

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

返回：

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

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

参数：`createdFrom`、`createdTo`。

返回动作分布：

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

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

参数：`createdFrom`、`createdTo`、`limit`，`limit` 默认10、最大100。

返回活跃用户：

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

## 16. 标准错误码

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

```text
INVALID_ARGUMENT
INVALID_CREDENTIALS
TOKEN_INVALID
TOKEN_EXPIRED
USER_DISABLED
AUTH_VERSION_MISMATCH
FORBIDDEN
SECURITY_LEVEL_FORBIDDEN
DOCUMENT_VIEW_FORBIDDEN
DOCUMENT_DOWNLOAD_FORBIDDEN
RESOURCE_NOT_FOUND
FILE_NOT_FOUND
UNSUPPORTED_FILE_TYPE
PREVIEW_UNAVAILABLE
PREVIEW_CONVERTER_UNAVAILABLE
PREVIEW_CONVERSION_TIMEOUT
PREVIEW_CONVERSION_FAILED
PAYLOAD_TOO_LARGE
BATCH_MANIFEST_MISMATCH
DATA_VERSION_CONFLICT
CATEGORY_IN_USE
MAIN_PLAN_HAS_CHILDREN
ATTACHMENT_IN_USE
ATTACHMENT_HAS_NO_ACL
SUB_PLAN_PERMISSION_INHERITED
DUPLICATE_PERMISSION_SUBJECT
INTERNAL_ERROR
DOCUMENT_NOT_DELETED
FILE_INTEGRITY_MISMATCH
FILE_PATH_INVALID
RESTORE_FILE_CHANGED
RESTORE_CATEGORY_INVALID
RESTORE_PARENT_INVALID
RESTORE_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 | 原方案分类无效，无法恢复 | `categoryId`、`reason=NOT_FOUND\|DELETED\|DISABLED` |
| `RESTORE_PARENT_INVALID` | 409 | 原所属主案无效，无法恢复 | `parentDocumentId`、`reason=NOT_FOUND\|DELETED\|WRONG_TYPE\|ROOT_MISMATCH` |
| `RESTORE_PERMISSION_INVALID` | 409 | 恢复后无法形成有效权限配置 | `invalidCount`、`reason=NO_VALID_ORGANIZATION_PERMISSION\|ALL_AUTHENTICATED_HAS_PERMISSIONS` |
| `RESTORE_RELATION_CONFLICT` | 409 | 历史权限或挂载关系与当前有效关系冲突 | `permissionConflictCount`、`bindingConflictCount` |

错误响应不得泄露绝对路径、哈希、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`必须固定包含以下14个数组：`roleCodes`、`allowedModules`、`documentTypes`、`documentStatuses`、`securityLevels`、`visibilityTypes`、`attachmentTypes`、`subjectTypes`、`allowedActions`、`categoryTypes`、`enabledStatuses`、`auditResults`、`auditActions`、`auditTargets`。

每个数组按 `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或后续版本。

## 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`仅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`。
