# 文档管理系统后端持久化实施指导(供 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 /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=/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或独立搜索服务前必须完成中文分词、相关性、权限过滤和迁移方案评审,不得在本阶段自行扩展。