# 文档管理系统后端持久化实施指导（供 Codex 使用）

> LibreOffice 离线文档转换与 PDF 预览同步版本：1.6。实现必须服从 `DMS_FUNCTION_CONTRACT.md` 1.4和 `DMS_API_CONTRACT.md` 1.4。

## 1. 文档目的

本文档用于指导 Codex 在本项目中实现文档管理系统的后端持久化能力。实施时必须以当前 React 前端已经表达的业务交互为范围，优先完成最小可用闭环，不扩展与当前任务无关的功能。

本文档是后续编码工作的基线。开始编码前，应先重新检查当前工作区、Git 状态和既有后端代码，避免覆盖用户修改。

## 2. 已确认的技术边界

### 2.1 本阶段必须实现

- 使用 MySQL 8.0 及以上版本保存结构化业务数据。
- 在当前项目后端根目录下使用 `backend/dms-storage/` 保存文档文件。
- 支持用户、组织机构、方案分类、主案、子方案、共享附件、附件挂载关系、文档权限和审计日志。
- 支持文档上传、查看、下载、编辑元数据和逻辑删除。
- 支持后台单文件上传和基础批量导入。
- 所有核心业务表包含创建时间、更新时间、删除时间和逻辑删除标记。
- 允许适当冗余，以降低接口查询复杂度。

### 2.2 本阶段明确不做

- 不修改前端界面设计。
- 不进行文档向量化。
- 不接入 Milvus。
- 不修改现有大模型抽取、标注和修复代码，除非后续用户明确要求。
- 不实现电子签章。
- 不实现审批流程。
- 不考虑国产数据库和国产操作系统适配。
- 不实现在线 Office 编辑。
- 不建设完整的动态 RBAC 权限平台。
- 不建设复杂文档历史版本系统。

### 2.3 推荐技术定位

- MySQL 是结构化业务数据的唯一事实来源。
- `backend/dms-storage/` 是文件内容的事实来源。
- 数据库只保存文件的相对路径，不保存文件二进制，不保存依赖具体机器的绝对路径。
- 现有 Python 信息抽取代码保持独立；本阶段不能把持久化改造与 AI 重构混在一起。

## 3. 实施原则

1. **最小表设计**：第一阶段只建立 7 张核心表。
2. **面向界面查询**：表字段优先满足登录、分类树、文档列表、子方案、共享附件、挂载关系、权限弹窗和日志审计页面。
3. **受控冗余**：允许冗余名称、路径、计数和快照字段，所有冗余由后端维护。
4. **文件与元数据分离**：MySQL 保存元数据，磁盘目录保存原始文件和衍生文件。
5. **逻辑删除**：业务查询默认附加 `is_deleted = 0`。
6. **后端强制鉴权**：前端隐藏按钮不能替代后端权限校验。
7. **失败可补偿**：文件写入和数据库事务无法形成真正的分布式事务，必须设计失败清理和补偿。
8. **接口版本化**：新增业务接口统一使用 `/api/v1` 前缀；现有 `/api` AI 接口暂不改动。
9. **时间统一**：数据库会话和后端统一使用 UTC 存储时间，对外返回 ISO 8601；前端按本地时区展示。
10. **安全路径**：任何客户端参数都不能直接拼接为磁盘路径。

## 4. 目录设计

存储根目录固定为：

```text
<project-root>/backend/dms-storage/
```

后续实现时创建以下目录：

```text
backend/dms-storage/
├── original/       # 正式原始文件
├── preview/        # PDF或其他预览衍生文件
├── extracted/      # 提取后的纯文本或结构化衍生文件
├── temporary/      # 上传过程中的临时文件
├── quarantine/     # 格式异常或校验失败文件
└── recycle/        # 预留目录；B8恢复不移动文件到此目录
```

### 4.1 Git 管理要求

- `backend/dms-storage/` 下的业务文件不得提交到 Git。
- 如果需要保留目录结构，可以在各目录中放置 `.gitkeep`，并通过 `.gitignore` 忽略其他内容。
- 后续实现时应检查现有 `.gitignore`，只做必要的最小修改。

### 4.2 文件相对路径规则

正式文件建议保存为：

```text
original/{yyyy}/{MM}/{documentId}/{fileUuid}.{extension}
```

示例：

```text
original/2026/07/125/8e79b8be-2cdd-4fbc-a459-268fd83bc230.docx
```

数据库中的 `file_relative_path` 只能保存上述相对路径。后端使用配置的存储根目录解析真实路径，并校验解析结果仍然位于 `backend/dms-storage/` 内。

## 5. 最小数据模型

第一阶段建立以下 7 张表：

| 表名 | 用途 |
|---|---|
| `sys_organization` | 组织机构树 |
| `sys_user` | 登录用户及固定角色 |
| `doc_category` | 方案计划分类树 |
| `doc_document` | 文档元数据、文件信息及主从关系 |
| `doc_attachment_binding` | 主案与共享附件的多对多挂载关系 |
| `doc_permission` | 组织和人员文档权限 |
| `sys_audit_log` | 登录、查看、下载、上传、删除和权限变更日志 |

本阶段不建立通用文档关系表、标签表、文件版本表、角色表、菜单表、审批表和向量索引表。附件挂载使用专用最小关系表。

## 6. E-R 图

```mermaid
erDiagram
    SYS_ORGANIZATION {
        bigint id PK
        bigint parent_id
        varchar org_code
        varchar org_name
        varchar ancestor_path
        varchar full_path_name
        int tree_level
        boolean is_deleted
        datetime created_at
        datetime updated_at
        datetime deleted_at
    }

    SYS_USER {
        bigint id PK
        varchar username
        varchar password_hash
        varchar real_name
        bigint organization_id FK
        varchar organization_name
        varchar role_code
        varchar security_level
        varchar status
        boolean is_deleted
        datetime created_at
        datetime updated_at
        datetime deleted_at
    }

    DOC_CATEGORY {
        bigint id PK
        bigint parent_id
        varchar category_code
        varchar category_name
        varchar category_type
        varchar ancestor_path
        varchar full_path_name
        int document_count
        boolean is_deleted
        datetime created_at
        datetime updated_at
        datetime deleted_at
    }

    DOC_DOCUMENT {
        bigint id PK
        varchar document_no
        varchar document_name
        varchar document_type
        bigint parent_document_id
        bigint root_document_id
        bigint category_id FK
        varchar category_name
        varchar category_path
        text summary
        varchar security_level
        varchar visibility_type
        varchar visibility_text
        json tags
        varchar document_status
        varchar file_relative_path
        varchar file_hash
        varchar version_no
        boolean is_deleted
        datetime created_at
        datetime updated_at
        datetime deleted_at
    }

    DOC_PERMISSION {
        bigint id PK
        bigint document_id FK
        varchar subject_type
        bigint subject_id
        varchar subject_name
        boolean can_view
        boolean can_download
        boolean can_edit
        boolean can_manage_permission
        boolean can_delete
        boolean is_deleted
        datetime created_at
        datetime updated_at
        datetime deleted_at
    }

    DOC_ATTACHMENT_BINDING {
        bigint id PK
        bigint main_document_id FK
        bigint attachment_document_id FK
        int sort_no
        boolean is_deleted
        datetime created_at
        datetime updated_at
        datetime deleted_at
    }

    SYS_AUDIT_LOG {
        bigint id PK
        bigint user_id
        varchar username
        bigint organization_id
        varchar organization_name
        varchar action_type
        varchar target_type
        bigint target_id
        varchar target_name
        varchar operation_result
        varchar client_ip
        varchar request_id
        json operation_detail
        datetime created_at
    }

    SYS_ORGANIZATION ||--o{ SYS_ORGANIZATION : "包含下级组织"
    SYS_ORGANIZATION ||--o{ SYS_USER : "拥有用户"
    DOC_CATEGORY ||--o{ DOC_CATEGORY : "包含子分类"
    DOC_CATEGORY ||--o{ DOC_DOCUMENT : "归类文档"
    DOC_DOCUMENT ||--o{ DOC_DOCUMENT : "包含子方案"
    DOC_DOCUMENT ||--o{ DOC_ATTACHMENT_BINDING : "主案挂载"
    DOC_DOCUMENT ||--o{ DOC_ATTACHMENT_BINDING : "附件被挂载"
    DOC_DOCUMENT ||--o{ DOC_PERMISSION : "配置权限"
    SYS_USER ||--o{ SYS_AUDIT_LOG : "产生操作日志"
    DOC_DOCUMENT ||--o{ SYS_AUDIT_LOG : "成为操作对象"
```

## 7. MySQL 逻辑字段设计

### 7.1 通用字段

除审计日志外，核心业务表统一使用：

```sql
created_by   BIGINT NULL,
updated_by   BIGINT NULL,
created_at   DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at   DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
             ON UPDATE CURRENT_TIMESTAMP(3),
deleted_at   DATETIME(3) NULL,
is_deleted   TINYINT(1) NOT NULL DEFAULT 0,
row_version  INT NOT NULL DEFAULT 0
```

更新业务数据时，使用类似条件：

```sql
UPDATE doc_document
SET document_name = ?, row_version = row_version + 1
WHERE id = ? AND is_deleted = 0 AND row_version = ?;
```

影响行数为 0 时，后端返回并发冲突，不得静默覆盖。

### 7.2 枚举值

第一阶段不建立字典表，使用后端枚举校验。

```text
用户角色 role_code:
USER, ADMIN

用户状态 status:
ENABLED, LOCKED, DISABLED

分类类型 category_type:
SCENE, STYLE, SITUATION, VERSION, OTHER

文档类型 document_type:
MAIN, SUB_PLAN, ATTACHMENT

文档状态 document_status:
DRAFT, PUBLISHED, ARCHIVED

权限主体 subject_type:
ORG, USER

权限来源 permission_source:
DIRECT, INHERITED

日志结果 operation_result:
SUCCESS, FAIL, DENIED
```

## 8. 表结构要求

### 8.1 `sys_organization`

必需字段：

```text
id                  BIGINT AUTO_INCREMENT PRIMARY KEY
parent_id           BIGINT NULL
org_code            VARCHAR(64) NOT NULL
org_name            VARCHAR(128) NOT NULL
ancestor_path       VARCHAR(1000) NOT NULL DEFAULT '/'
full_path_name      VARCHAR(1000) NOT NULL
tree_level          INT NOT NULL DEFAULT 1
sort_no             INT NOT NULL DEFAULT 0
status              VARCHAR(20) NOT NULL DEFAULT 'ENABLED'
created_by          BIGINT NULL
updated_by          BIGINT NULL
created_at          DATETIME(3)
updated_at          DATETIME(3)
deleted_at          DATETIME(3) NULL
is_deleted          TINYINT(1) NOT NULL DEFAULT 0
row_version         INT NOT NULL DEFAULT 0
```

索引：

```text
UNIQUE(org_code) 或采用有效记录唯一策略
INDEX(parent_id, is_deleted, sort_no)
INDEX(status, is_deleted)
```

删除有下级组织或有效用户的组织时，默认拒绝，不级联删除。

### 8.2 `sys_user`

必需字段：

```text
id                  BIGINT AUTO_INCREMENT PRIMARY KEY
username            VARCHAR(64) NOT NULL
password_hash       VARCHAR(255) NOT NULL
real_name           VARCHAR(64) NOT NULL
organization_id     BIGINT NULL
organization_name   VARCHAR(128) NULL
role_code           VARCHAR(32) NOT NULL DEFAULT 'USER'
security_level      VARCHAR(32) NULL
status              VARCHAR(20) NOT NULL DEFAULT 'ENABLED'
last_login_at       DATETIME(3) NULL
last_login_ip       VARCHAR(64) NULL
created_by          BIGINT NULL
updated_by          BIGINT NULL
created_at          DATETIME(3)
updated_at          DATETIME(3)
deleted_at          DATETIME(3) NULL
is_deleted          TINYINT(1) NOT NULL DEFAULT 0
row_version         INT NOT NULL DEFAULT 0
```

要求：

- 密码只保存强哈希，禁止明文和可逆加密。
- 用户名按业务规则唯一。
- 登录成功和失败都记录审计日志。
- 禁用或逻辑删除用户后，其历史日志快照仍然保留。

### 8.3 `doc_category`

必需字段：

```text
id                  BIGINT AUTO_INCREMENT PRIMARY KEY
parent_id           BIGINT NULL
category_code       VARCHAR(64) NOT NULL
category_name       VARCHAR(128) NOT NULL
category_type       VARCHAR(32) NOT NULL DEFAULT 'OTHER'
ancestor_path       VARCHAR(1000) NOT NULL DEFAULT '/'
full_path_name      VARCHAR(1000) NOT NULL
tree_level          INT NOT NULL DEFAULT 1
document_count      INT NOT NULL DEFAULT 0
sort_no             INT NOT NULL DEFAULT 0
status              VARCHAR(20) NOT NULL DEFAULT 'ENABLED'
created_by          BIGINT NULL
updated_by          BIGINT NULL
created_at          DATETIME(3)
updated_at          DATETIME(3)
deleted_at          DATETIME(3) NULL
is_deleted          TINYINT(1) NOT NULL DEFAULT 0
row_version         INT NOT NULL DEFAULT 0
```

要求：

- 数据库不强制只能有四层分类。
- 修改分类名称或移动分类时，后端必须更新全部后代的 `ancestor_path` 和 `full_path_name`。
- 同步更新关联文档冗余的 `category_name` 和 `category_path`。
- 删除存在子分类或有效文档的分类时默认拒绝。

### 8.4 `doc_document`

必需字段：

```text
id                       BIGINT AUTO_INCREMENT PRIMARY KEY
document_no              VARCHAR(64) NULL
document_name            VARCHAR(255) NOT NULL
document_type            VARCHAR(32) NOT NULL
parent_document_id       BIGINT NULL
root_document_id         BIGINT NULL
category_id              BIGINT NULL
category_name            VARCHAR(128) NULL
category_path            VARCHAR(1000) NULL
summary                  TEXT NULL
security_level           VARCHAR(32) NOT NULL DEFAULT 'INTERNAL'
visibility_type          VARCHAR(32) NOT NULL DEFAULT 'CUSTOM'
visibility_text          VARCHAR(500) NULL
tags                     JSON NULL
search_text              TEXT NULL
document_status          VARCHAR(32) NOT NULL DEFAULT 'PUBLISHED'
version_no               VARCHAR(32) NOT NULL DEFAULT 'V1'
sort_no                  INT NOT NULL DEFAULT 0
file_original_name       VARCHAR(255) NOT NULL
file_storage_name        VARCHAR(255) NOT NULL
file_relative_path       VARCHAR(1000) NOT NULL
file_extension           VARCHAR(20) NOT NULL
file_mime_type           VARCHAR(128) NULL
file_size                BIGINT NOT NULL DEFAULT 0
file_hash                CHAR(64) NOT NULL
preview_relative_path    VARCHAR(1000) NULL
extracted_text_path      VARCHAR(1000) NULL
child_count              INT NOT NULL DEFAULT 0
attachment_count         INT NOT NULL DEFAULT 0
view_count               BIGINT NOT NULL DEFAULT 0
download_count           BIGINT NOT NULL DEFAULT 0
last_viewed_at           DATETIME(3) NULL
created_by               BIGINT NULL
created_by_name          VARCHAR(64) NULL
updated_by               BIGINT NULL
updated_by_name          VARCHAR(64) NULL
created_at               DATETIME(3)
updated_at               DATETIME(3)
deleted_at               DATETIME(3) NULL
is_deleted               TINYINT(1) NOT NULL DEFAULT 0
row_version              INT NOT NULL DEFAULT 0
```

关系规则：

```text
MAIN:
  parent_document_id = NULL
  root_document_id = 自身ID

SUB_PLAN:
  parent_document_id = 所属主案ID
  root_document_id = 所属主案ID

ATTACHMENT:
  parent_document_id = NULL
  root_document_id = NULL
  通过 doc_attachment_binding 挂载到一个或多个主案
```

第一阶段约束：

- 一个子方案只能属于一个主案。
- 一个共享附件可以挂载到多个主案。
- 同一主案不能重复挂载同一共享附件。
- 共享附件对全部已登录用户共享，不配置组织或人员ACL。
- 一个文档只能选择一个方案分类。
- 不允许子方案继续嵌套子方案。
- 不允许将文档关联到自身。

`search_text` 是有意冗余字段，由后端拼接文档名称、概述、标签等内容，第一阶段用于普通数据库检索；不得在本阶段接入向量逻辑。

建议索引：

```text
INDEX(document_type, is_deleted, updated_at)
INDEX(parent_document_id, document_type, is_deleted)
INDEX(root_document_id, is_deleted)
INDEX(category_id, is_deleted)
INDEX(document_status, is_deleted)
INDEX(security_level, is_deleted)
INDEX(file_hash)
INDEX(created_by, is_deleted)
INDEX(is_deleted, deleted_at, id)  # B8由0002迁移增加
```

### 8.5 `doc_permission`

必需字段：

```text
id                       BIGINT AUTO_INCREMENT PRIMARY KEY
document_id              BIGINT NOT NULL
subject_type             VARCHAR(20) NOT NULL
subject_id               BIGINT NOT NULL
subject_name             VARCHAR(255) NOT NULL
can_view                 TINYINT(1) NOT NULL DEFAULT 1
can_download             TINYINT(1) NOT NULL DEFAULT 0
can_edit                 TINYINT(1) NOT NULL DEFAULT 0
can_manage_permission    TINYINT(1) NOT NULL DEFAULT 0
can_delete               TINYINT(1) NOT NULL DEFAULT 0
permission_source        VARCHAR(20) NOT NULL DEFAULT 'DIRECT'
created_by               BIGINT NULL
updated_by               BIGINT NULL
created_at               DATETIME(3)
updated_at               DATETIME(3)
deleted_at               DATETIME(3) NULL
is_deleted               TINYINT(1) NOT NULL DEFAULT 0
row_version              INT NOT NULL DEFAULT 0
```

要求：

- `subject_type = ORG` 时，`subject_id` 表示组织ID。
- `subject_type = USER` 时，`subject_id` 表示用户ID。
- 同一有效文档、主体类型和主体ID只能有一条有效权限记录。
- 删除权限采用逻辑删除。
- 新增子方案时，可以复制主案权限并标记为 `INHERITED`，以换取简单查询。
- 共享附件不写入本权限表，所有状态正常的已登录用户均可查看和下载。
- 保存权限必须在一个数据库事务内完成。

建议索引：

```text
INDEX(document_id, is_deleted)
INDEX(subject_type, subject_id, is_deleted)
INDEX(document_id, subject_type, subject_id, is_deleted)
```

### 8.6 `doc_attachment_binding`

必需字段：

```text
id                       BIGINT AUTO_INCREMENT PRIMARY KEY
main_document_id         BIGINT NOT NULL
attachment_document_id   BIGINT NOT NULL
sort_no                  INT NOT NULL DEFAULT 0
created_by               BIGINT NULL
updated_by               BIGINT NULL
created_at               DATETIME(3)
updated_at               DATETIME(3)
deleted_at               DATETIME(3) NULL
is_deleted               TINYINT(1) NOT NULL DEFAULT 0
row_version              INT NOT NULL DEFAULT 0
```

要求：

- `main_document_id`必须指向有效的`MAIN`文档。
- `attachment_document_id`必须指向有效的`ATTACHMENT`文档。
- 同一主案与同一附件只能有一条有效挂载关系。
- 解除挂载只逻辑删除本表记录，不删除附件文档和磁盘文件。
- 新增、解除挂载与主案`attachment_count`更新必须在一个事务中完成。
- 删除已挂载附件时返回冲突，第一阶段不自动解除全部挂载。

建议索引：

```text
INDEX(main_document_id, is_deleted, sort_no)
INDEX(attachment_document_id, is_deleted)
INDEX(main_document_id, attachment_document_id, is_deleted)
```

### 8.7 `sys_audit_log`

必需字段：

```text
id                    BIGINT AUTO_INCREMENT PRIMARY KEY
user_id               BIGINT NULL
username              VARCHAR(64) NULL
real_name             VARCHAR(64) NULL
organization_id       BIGINT NULL
organization_name     VARCHAR(128) NULL
action_type           VARCHAR(32) NOT NULL
target_type           VARCHAR(32) NULL
target_id             BIGINT NULL
target_name           VARCHAR(255) NULL
operation_result      VARCHAR(20) NOT NULL
failure_reason        VARCHAR(1000) NULL
client_ip             VARCHAR(64) NULL
user_agent            VARCHAR(500) NULL
request_id            VARCHAR(64) NULL
operation_detail      JSON NULL
created_at            DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
updated_at            DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
deleted_at            DATETIME(3) NULL
is_deleted            TINYINT(1) NOT NULL DEFAULT 0
```

审计日志保存操作发生时的名称快照，不依赖用户、组织或文档当前名称。

需要记录的操作至少包括：

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

建议索引：

```text
INDEX(created_at)
INDEX(user_id, created_at)
INDEX(action_type, created_at)
INDEX(target_type, target_id, created_at)
INDEX(operation_result, created_at)
INDEX(request_id)
```

普通业务代码不得更新或删除审计日志。

## 9. 逻辑删除规则

### 9.1 通用查询

所有正常查询默认包含：

```sql
WHERE is_deleted = 0
```

### 9.2 删除文档

删除文档时必须：

1. 检查当前用户删除权限。
2. 更新文档 `is_deleted = 1` 和 `deleted_at`。
3. 逻辑删除文档权限。
4. 删除子方案时更新所属主案的`child_count`；解除附件挂载时更新`attachment_count`。
5. 保留原始文件，不立即物理删除。
6. 写入删除审计日志。

删除主案时，如果存在有效子方案，第一阶段默认拒绝；主案的附件挂载关系可以随主案逻辑删除，但共享附件本身必须保留。

### 9.3 恢复文档

正式接口只允许：

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

旧 `/api/v1/documents/{id}/restore` 永久废止，不得实现重定向、别名或兼容调用。

仅ADMIN可以查询回收站和单条恢复MAIN、SUB_PLAN或ATTACHMENT。不恢复分类、用户或组织，不支持批量恢复、物理删除、自动清理、独立恢复ACL或挂载关系，也不移动文件到 `recycle/`。

回收站固定查询 `is_deleted=1 AND deleted_at IS NOT NULL`。支持keyword、documentType、categoryId、deletedFrom、deletedTo、分页和受限排序；默认 `deleted_at DESC, id DESC`。删除人使用最近一次匹配的成功 `DELETE_DOCUMENT`审计窗口查询获取，无可靠审计时返回null，不得用 `updated_by`冒充。

通用恢复必须验证：

- 记录当前已逻辑删除且rowVersion一致；
- 文件路径是存储根目录内的安全相对路径；
- 文件存在且为普通文件；
- 文件大小和SHA-256一致；
- 扩展名、文件头及Office容器结构一致；
- 所属分类仍有效且启用；
- SUB_PLAN所属父主案和根主案指向同一有效MAIN；
- 恢复不会造成有效ACL或有效挂载唯一冲突。

文件检查使用两阶段方案：事务前完成哈希和格式校验，保持文件句柄并记录文件状态；事务内锁定记录后、提交前复核状态。变化时返回 `RESTORE_FILE_CHANGED`。数据库事务无法锁定磁盘文件，当前以应用独占管理UUID文件为前提。恢复不移动、复制或删除文件。

MAIN采用关系恢复方案B：通过相同documentId、deletedAt和updatedBy识别本次删除批次，只恢复其中仍有效且不冲突的ACL和挂载关系。无效或禁用主体、已删除或不存在附件跳过并计数；有效关系冲突整体回滚。

- ORGANIZATION最终必须至少有一个有效且可查看的ORG权限；
- CUSTOM允许空权限，普通用户默认无权访问；
- ALL_AUTHENTICATED最终ACL必须为空；
- 恢复后重算分类documentCount、主案childCount和attachmentCount。

SUB_PLAN不恢复独立ACL，恢复后继承主案当前最新权限并重算父主案childCount。ATTACHMENT继续固定PUBLIC和ALL_AUTHENTICATED，不恢复ACL或历史挂载；异常存在有效挂载时拒绝恢复。

文档名称、原文件名、分类下名称、同一主案下子方案名称和文件哈希允许重复。不同文档可以使用相同哈希，但继续使用各自独立文件路径。

恢复文档、恢复关系、重算计数和成功 `RESTORE_DOCUMENT`审计必须原子提交。恢复响应必须纯序列化，不增加viewCount或产生 `VIEW_DOCUMENT`审计。最小范围内恢复失败不写业务审计。

## 10. 文件处理规则

### 10.1 允许的文件类型

第一阶段建议明确允许：

```text
.doc
.docx
.pdf
.xls
.xlsx
```

不能只检查扩展名，还要检查文件头或 MIME 类型。无法确认格式的文件放入 `quarantine/`，不得进入正式目录。

### 10.2 文件命名

- 正式磁盘文件名使用 UUID。
- 原始文件名保存在数据库。
- 禁止使用前端上传文件名拼接目录。
- 扩展名统一转为小写。
- 必须拒绝包含路径穿越含义的输入。

### 10.3 上传事务顺序

```mermaid
flowchart TD
    A[接收上传] --> B[写入 temporary]
    B --> C[校验文件类型与大小]
    C --> D[计算 SHA-256]
    D --> E[创建文档数据库记录]
    E --> F[创建正式目录]
    F --> G[原子移动到 original]
    G --> H[更新相对路径]
    H --> I[提交数据库事务]
    I --> J[记录审计日志]
```

失败补偿：

- 数据库失败：删除临时文件或已移动的孤儿文件。
- 文件移动失败：回滚数据库事务。
- 审计日志失败：业务操作应记录错误并告警；是否回滚由实现阶段统一决定，默认核心上传成功不因非关键统计更新失败而丢失文件。

### 10.4 下载和预览

文件不能通过 Flask 静态目录直接暴露。必须经过后端接口：

1. 查询文档有效状态。
2. 检查用户角色、密级和文档权限。
3. 解析并校验相对路径。
4. 以流式响应返回文件。
5. 更新查看或下载计数。
6. 写入审计日志。

DOC、DOCX 预览：由 LibreOffice 离线转换为 PDF 后返回，转换失败返回统一错误码；转换衍生的 PDF 缓存以原文件 SHA-256 为键写入 `preview/`；首次转换原子落盘，后续直接命中缓存。PDF 直接返回原始文件。XLS、XLSX 不支持在线预览。

### 10.5 文件完整性

- 上传时计算 SHA-256 并保存到 `file_hash`。
- 下载或定期巡检时可以重新计算哈希。
- 数据库有记录但文件不存在时，接口返回明确的“文件缺失”错误并写入失败日志。
- 不得在数据库中保存 Windows 盘符绝对路径。

## 11. 前端交互对应接口

新增业务接口使用 `/api/v1`。接口响应必须采用统一结构，并区分业务错误、权限错误和系统错误。

### 11.1 登录

```text
POST /api/v1/auth/login
POST /api/v1/auth/logout
GET  /api/v1/users/me
```

### 11.2 组织机构

```text
GET /api/v1/organizations/tree
GET /api/v1/users?organizationId=&keyword=
```

### 11.3 方案分类

```text
GET    /api/v1/categories/tree
POST   /api/v1/categories
PUT    /api/v1/categories/{id}
DELETE /api/v1/categories/{id}
```

### 11.4 文档浏览

```text
GET /api/v1/documents
GET /api/v1/documents/{id}
GET /api/v1/documents/{id}/children
GET /api/v1/documents/{id}/materials
GET /api/v1/documents/recent
GET /api/v1/documents/search?keyword=
```

列表接口至少支持：

```text
page
pageSize
documentType
categoryId
keyword
securityLevel
status
sortBy
sortDirection
```

### 11.5 文档管理和文件

```text
POST   /api/v1/documents
PUT    /api/v1/documents/{id}
DELETE /api/v1/documents/{id}
GET    /api/v1/documents/{id}/preview
GET    /api/v1/documents/{id}/download
POST   /api/v1/documents/batch-import
```

`POST /documents` 使用 `multipart/form-data`，同时提交文件和文档元数据。批量导入第一阶段可以同步返回逐文件结果，但应限制文件数量和请求总大小。

文档恢复使用独立回收站资源：

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

旧 `/api/v1/documents/{id}/restore` 永久废止。

### 11.6 权限

```text
GET /api/v1/documents/{id}/permissions
PUT /api/v1/documents/{id}/permissions
```

`PUT` 应接收完整权限集合，在一个事务内完成新增、更新和逻辑删除。

### 11.7 审计与统计

```text
GET /api/v1/audit/logs
GET /api/v1/audit/statistics/trend
GET /api/v1/audit/statistics/actions
GET /api/v1/audit/statistics/users
GET /api/v1/statistics/documents
```

统计数据从 `doc_document` 和 `sys_audit_log` 查询，不在第一阶段额外建立统计表。

## 12. 权限判定规则

第一阶段采用固定角色加文档 ACL：

1. `ADMIN` 可以进入后台管理，但文档下载等敏感操作仍应记录审计。
2. `ADMIN` 可以查看审计日志并具有 `AUDIT_LOG` 模块；`USER`不能访问审计接口。
3. `USER` 只能访问拥有查看权限的文档。
4. 用户直接权限优先于组织权限。
5. 没有任何有效权限记录时默认拒绝，不得默认全员可见，除非文档明确设置为全员可见。
6. 查看、下载、编辑、权限管理和删除分别判断，不合并成一个布尔值。
7. 文档密级检查必须在 ACL 之前或同时进行。

权限查询可以使用：

- 用户本人 `subject_type = USER` 的记录；
- 用户所属组织及其祖先组织 `subject_type = ORG` 的记录；
- 文档默认可见范围。

后续编码前应先确定“直接拒绝是否覆盖组织允许”。第一阶段若不实现显式拒绝，则权限表只表达允许，查询逻辑更简单。

## 13. 冗余数据维护规则

以下字段不得由前端自由指定：

- `organization_name`
- `category_name`
- `category_path`
- `created_by_name`
- `updated_by_name`
- `subject_name`
- `child_count`
- `attachment_count`
- `view_count`
- `download_count`
- `search_text`
- 审计日志中的用户、组织和目标名称快照

后端维护规则：

- 组织改名：同步更新有效用户的 `organization_name`；历史日志不更新。
- 分类改名或移动：同步更新有效文档的分类名称和路径。
- 新增或删除子方案：更新主案 `child_count`。
- 新增或解除附件挂载：更新主案`attachment_count`，不得复制或删除共享附件文件。
- 文档名称、摘要或标签改变：重新生成 `search_text`。
- 权限主体改名：可以同步有效权限的 `subject_name`；历史日志不更新。

## 14. 配置要求

不要在代码中写死密码和环境相关配置。建议使用环境变量：

```text
DMS_DB_HOST=127.0.0.1
DMS_DB_PORT=3306
DMS_DB_NAME=dms
DMS_DB_USER=dms_app
DMS_DB_PASSWORD=change-me
DMS_STORAGE_ROOT=<project-root>/backend/dms-storage
DMS_MAX_FILE_SIZE_MB=100
DMS_BATCH_MAX_FILES=50
DMS_OFFICE_PREVIEW_ENABLED=true
DMS_LIBREOFFICE_EXECUTABLE=C:\Program Files\LibreOffice\program\soffice.exe
DMS_LIBREOFFICE_TIMEOUT_SECONDS=60
DMS_LIBREOFFICE_MAX_CONCURRENCY=2
```

本地开发可以提供不包含真实密码的 `.env.example`。真实 `.env` 必须被 Git 忽略。

数据库要求：

```text
MySQL 8.0+
字符集 utf8mb4
排序规则优先 utf8mb4_0900_ai_ci，若需要严格区分则在实现时确认
存储引擎 InnoDB
事务隔离级别默认 READ COMMITTED 或 MySQL 默认级别，实施时统一选择并记录
```

## 15. 建议实施顺序

Codex 后续编码时按以下顺序进行，每一步完成后先验证，再继续下一步。

### 阶段 1：基础工程和配置

- [ ] 检查现有 Flask 后端结构和依赖。
- [ ] 选择轻量 ORM 或 SQL 访问方案，不与现有 AI 模块耦合。
- [ ] 增加 MySQL 配置读取。
- [ ] 增加统一数据库连接和事务管理。
- [ ] 增加统一 API 响应和错误处理。
- [ ] 创建 `backend/dms-storage/` 目录结构并配置 Git 忽略。

### 阶段 2：数据库迁移

- [ ] 创建 7 张核心表。
- [ ] 创建索引和必要外键。
- [ ] 提供可重复执行的初始化或迁移方案。
- [ ] 插入最小组织、管理员、审计员和普通用户测试数据。
- [ ] 密码必须使用哈希生成，不在 SQL 中保存已知明文密码。

### 阶段 3：登录、组织和分类

- [ ] 实现登录、退出和当前用户接口。
- [ ] 实现组织树查询。
- [ ] 实现人员查询。
- [ ] 实现分类树增删改查。
- [ ] 验证分类路径冗余同步。

### 阶段 4：文档和文件存储

- [ ] 实现单文件上传。
- [ ] 实现文件类型和大小校验。
- [ ] 实现 SHA-256。
- [ ] 实现数据库事务与文件补偿。
- [ ] 实现文档列表、详情和编辑。
- [ ] 实现主案、子方案、共享附件和附件挂载关系查询。
- [ ] 实现逻辑删除。
- [ ] 按正式回收站资源实现单条恢复，保持旧恢复路径废止。
- [ ] 实现权限校验后的预览和下载。

### 阶段 5：权限和审计

- [ ] 实现组织和人员权限读取。
- [ ] 实现权限集合事务保存。
- [ ] 实现查看、下载、编辑、删除和权限管理校验。
- [ ] 为关键操作写入审计日志。
- [ ] 实现日志搜索和统计接口。

### 阶段 6：批量导入与联调

- [ ] 实现受限数量的基础批量导入。
- [ ] 返回逐文件成功或失败结果。
- [ ] 防止单个失败文件回滚全部成功文件，除非产品明确要求全有或全无。
- [ ] 与当前前端逐个按钮联调。
- [ ] 清除所有模拟数据依赖前，确保接口数据完整可用。

## 16. 测试和验收要求

### 16.1 数据库

- [ ] 所有正常查询不返回逻辑删除数据。
- [ ] 更新时可以检测 `row_version` 冲突。
- [ ] 分类和组织树路径正确。
- [ ] 主案计数与子方案、有效附件挂载关系实际数量一致。
- [ ] 权限保存不存在重复有效记录。

### 16.2 文件

- [ ] 方案名称按正式唯一范围校验；未经用户确认不得覆盖，同名覆盖保留原文档ID和关系，并以事务方式替换文件。
- [ ] 非法扩展名和伪造文件头被拒绝。
- [ ] 数据库写入失败不会留下正式孤儿文件。
- [ ] 文件移动失败不会留下有效数据库记录。
- [ ] 下载不能通过 `../` 等方式访问存储根目录外文件。
- [ ] 文件哈希与实际文件一致。
- [ ] 逻辑删除后不能继续预览或下载。

### 16.3 权限

- [ ] 无权限用户不能查看元数据、预览或下载。
- [ ] 只有查看权限时不能下载。
- [ ] 普通用户不能进入管理接口。
- [ ] 审计员不能修改文档。
- [ ] 权限变更有完整日志。

### 16.4 前端交互

- [ ] 登录页使用真实账号验证。
- [ ] 分类树来自数据库。
- [ ] 文档列表支持分页和搜索。
- [ ] 点击主案能够查询子方案和已挂载共享附件。
- [ ] 查看和下载返回真实文件。
- [ ] 上传和批量导入能够生成数据库记录与磁盘文件。
- [ ] 编辑、权限和删除按钮具有真实效果。
- [ ] 日志表格和统计图来自真实审计数据。

## 17. 完成定义

本阶段只有同时满足以下条件才算完成：

1. MySQL 中存在最小 7 表结构及必要索引。
2. `backend/dms-storage/` 按约定保存真实文件。
3. 前端主要页面不再依赖内置模拟文档数据。
4. 登录、分类、文档、权限和日志形成完整业务闭环。
5. 文档上传、数据库写入和磁盘存储具备失败补偿。
6. 所有删除采用逻辑删除，原始文件不会立即丢失。
7. 所有文档读取和下载经过后端权限校验。
8. 关键行为产生真实审计日志。
9. 现有 AI 抽取与标注代码没有被无关重构或破坏。
10. 自动化测试覆盖文件路径安全、逻辑删除、权限和关键接口。

## 18. 后续扩展预留

以下内容不在当前任务中，但未来可以在不破坏现有模型的情况下增加：

- `doc_file_version`：文档历史版本。
- `doc_relation`：一个资料关联多个主案或通用关系。
- `doc_tag`、`doc_document_tag`：标签管理和统计。
- `sys_role`、`sys_permission`、`sys_user_role`：动态 RBAC。
- `doc_import_job`、`doc_import_item`：持久化批量任务。
- `doc_vector_index`：未来记录 Milvus 索引状态。
- 审批、签章和发布流程。

未经用户明确确认，Codex 不应在当前阶段提前实现这些扩展。

## 19. 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 1.1.0。

固定锁顺序：

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

rowVersion规则：

- 文档恢复成功递增一次；
- 每条恢复ACL和挂载分别递增一次；
- 分类计数变化时分类递增一次；
- SUB_PLAN恢复时父主案递增一次；
- MAIN计数更新包含在文档自身一次递增中；
- 跳过关系不修改版本。

新增错误码：

```text
DOCUMENT_NOT_DELETED           409
FILE_INTEGRITY_MISMATCH        409
FILE_PATH_INVALID              500
RESTORE_FILE_CHANGED           409
RESTORE_CATEGORY_INVALID       409
RESTORE_PARENT_INVALID         409
RESTORE_PERMISSION_INVALID     409
RESTORE_RELATION_CONFLICT      409
```

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

## 20. Q1-B统一UI字典实施指导

- 默认配置文件使用 `backend/dms/resources/ui-dictionaries.zh-CN.json`，部署覆盖变量为 `DMS_UI_DICTIONARY_CONFIG_PATH`。
- 配置必须在DMS应用初始化期间读取、严格校验并转换为不可变内存对象；任一字典无效即终止启动，不得部分加载或静默回退。
- 14组字典代码集合必须分别与 `backend/dms/common/enums.py` 的对应枚举完全一致，配置只能调整中文标签和排序号，不能增加或修改英文代码。
- `GET /api/v1/config/ui-dictionaries`仅使用已加载内存对象。除现有认证校验外，不得查询业务数据库，不得写审计日志，不得读取磁盘。
- 响应项目按 `sortNo`、`code`排序，遵循统一响应、requestId和 `/api/v1` CORS规则，不返回外部配置绝对路径、加载时间、原始文件内容或内部错误。
- 本阶段不提供热更新或配置写接口；修改配置后重启各服务进程生效。
- 不新增数据库表、字段或迁移，不修改既有枚举值和业务接口语义。
- OpenAPI升级为 `1.2.0`，只增加字典查询路径及对应DTO、401和500响应。

## 21. Q2-B更新时间范围检索实施指导

- 使用一个公共解析方法处理三个接口的 `updatedFrom`、`updatedTo`，要求ISO 8601且包含明确时区，统一转换为UTC无时区值后与MySQL UTC `DATETIME`比较。
- `updatedFrom`采用 `>=`，`updatedTo`采用 `<=`；同时存在时先验证开始时间不晚于结束时间。
- 方案和子方案过滤 `doc_document.updated_at`；已挂载附件仍过滤附件对应的 `doc_document.updated_at`，禁止过滤 `doc_attachment_binding`时间或排序号。
- SQLAlchemy `where`条件必须在查询执行和分页前形成，不得在Python中进行时间过滤。
- 保持权限、密级、状态、排序、字符串ID、DTO、计数和审计不变。
- Q2-B不新增索引、迁移或数据库对象；当时OpenAPI升级为 `1.3.0`。

## 17. 1.6 LibreOffice 转换持久化补充

1. 使用唯一迁移`0003_q3_business_alignment_and_content_search`：`doc_document.search_text`升级为LONGTEXT，新增`content_text LONGTEXT NULL`、`content_extract_status VARCHAR(32)`、`content_extracted_at DATETIME(3) NULL`，并将用户角色CHECK收敛为USER、ADMIN。
2. 历史AUDITOR在迁移中递增auth_version和row_version、置为DISABLED并逻辑删除；为满足新CHECK可降为USER，但不得提升为ADMIN，历史审计快照保持不变。
3. 上传文件先在暂存区完成类型校验、哈希和正文提取，再在数据库事务内保存元数据，提交后原子移动；提取FAILED不得导致原文件丢失。编辑名称、摘要或标签时以已有content_text重建search_text。
4. PDF正文从文本层提取；DOCX提取普通段落、表格及可安全读取的页眉页脚；DOC为UNSUPPORTED。正文与组合搜索文本只存MySQL，不存二进制或HTML，不引入Milvus、Elasticsearch或OpenSearch。
5. 回填命令`python -m dms.backfill_document_content`要求显式数据库URL并校验`SELECT DATABASE()`严格等于`dms_test`，不得自动运行或连接dms主库。
6. preview 统一路径 `/api/v1/documents/{id}/preview`：PDF 直接读取原始文件；DOC、DOCX 命中 `preview/{fileHash}.pdf` 缓存，未命中则启动 LibreOffice 转换，原子写入缓存后返回。转换配置通过环境变量 `DMS_OFFICE_PREVIEW_ENABLED`、`DMS_LIBREOFFICE_EXECUTABLE`、`DMS_LIBREOFFICE_TIMEOUT_SECONDS`、`DMS_LIBREOFFICE_MAX_CONCURRENCY` 提供。不调用 Microsoft Office 或 WPS。
7. DOC 上传时由 LibreOffice 转换为 PDF 后提取正文；DOCX 仍使用原生 DOCX 解析提取正文。
8. 提供显式回填命令 `python -m dms.backfill_preview_cache --dry-run` 为既有 DOC/DOCX 生成预览缓存；`--limit` 限制批量大小；支持幂等执行，已有有效缓存跳过。
7. 正式部署前必须备份数据库并依次应用`0002_add_restore_audit_action`与`0003_q3_business_alignment_and_content_search`。若正文已写入或LONGTEXT内容无法安全降为TEXT，0003 downgrade应明确阻止而非静默丢失。
8. 当前正文检索使用参数化、转义后的`LIKE`满足现阶段数据规模；随着文档数量和正文体积增长，会出现大文本扫描性能风险。引入MySQL FULLTEXT或独立搜索服务前必须完成中文分词、相关性、权限过滤和迁移方案评审，不得在本阶段自行扩展。
