CODEX_BACKEND_PERSISTENCE_GUIDE.md 40 KB

文档管理系统后端持久化实施指导(供 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. 目录设计

存储根目录固定为:

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

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

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

4.1 Git 管理要求

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

4.2 文件相对路径规则

正式文件建议保存为:

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

示例:

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 图

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 通用字段

除审计日志外,核心业务表统一使用:

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

更新业务数据时,使用类似条件:

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

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

7.2 枚举值

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

用户角色 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

必需字段:

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

索引:

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

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

8.2 sys_user

必需字段:

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

必需字段:

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_pathfull_path_name
  • 同步更新关联文档冗余的 category_namecategory_path
  • 删除存在子分类或有效文档的分类时默认拒绝。

8.4 doc_document

必需字段:

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

关系规则:

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 是有意冗余字段,由后端拼接文档名称、概述、标签等内容,第一阶段用于普通数据库检索;不得在本阶段接入向量逻辑。

建议索引:

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

必需字段:

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,以换取简单查询。
  • 共享附件不写入本权限表,所有状态正常的已登录用户均可查看和下载。
  • 保存权限必须在一个数据库事务内完成。

建议索引:

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

必需字段:

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更新必须在一个事务中完成。
  • 删除已挂载附件时返回冲突,第一阶段不自动解除全部挂载。

建议索引:

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

必需字段:

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

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

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

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

建议索引:

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 通用查询

所有正常查询默认包含:

WHERE is_deleted = 0

9.2 删除文档

删除文档时必须:

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

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

9.3 恢复文档

正式接口只允许:

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 允许的文件类型

第一阶段建议明确允许:

.doc
.docx
.pdf
.xls
.xlsx

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

10.2 文件命名

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

10.3 上传事务顺序

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 登录

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

11.2 组织机构

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

11.3 方案分类

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

11.4 文档浏览

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=

列表接口至少支持:

page
pageSize
documentType
categoryId
keyword
securityLevel
status
sortBy
sortDirection

11.5 文档管理和文件

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,同时提交文件和文档元数据。批量导入第一阶段可以同步返回逐文件结果,但应限制文件数量和请求总大小。

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

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

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

11.6 权限

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

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

11.7 审计与统计

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_documentsys_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. 配置要求

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

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 忽略。

数据库要求:

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_tagdoc_document_tag:标签管理和统计。
  • sys_rolesys_permissionsys_user_role:动态 RBAC。
  • doc_import_jobdoc_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计数更新包含在文档自身一次递增中;
  • 跳过关系不修改版本。

新增错误码:

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仅使用已加载内存对象。除现有认证校验外,不得查询业务数据库,不得写审计日志,不得读取磁盘。
  • 响应项目按 sortNocode排序,遵循统一响应、requestId和 /api/v1 CORS规则,不返回外部配置绝对路径、加载时间、原始文件内容或内部错误。
  • 本阶段不提供热更新或配置写接口;修改配置后重启各服务进程生效。
  • 不新增数据库表、字段或迁移,不修改既有枚举值和业务接口语义。
  • OpenAPI升级为 1.2.0,只增加字典查询路径及对应DTO、401和500响应。

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

  • 使用一个公共解析方法处理三个接口的 updatedFromupdatedTo,要求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_searchdoc_document.search_text升级为LONGTEXT,新增content_text LONGTEXT NULLcontent_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_ENABLEDDMS_LIBREOFFICE_EXECUTABLEDMS_LIBREOFFICE_TIMEOUT_SECONDSDMS_LIBREOFFICE_MAX_CONCURRENCY 提供。不调用 Microsoft Office 或 WPS。
  7. DOC 上传时由 LibreOffice 转换为 PDF 后提取正文;DOCX 仍使用原生 DOCX 解析提取正文。
  8. 提供显式回填命令 python -m dms.backfill_preview_cache --dry-run 为既有 DOC/DOCX 生成预览缓存;--limit 限制批量大小;支持幂等执行,已有有效缓存跳过。
  9. 正式部署前必须备份数据库并依次应用0002_add_restore_audit_action0003_q3_business_alignment_and_content_search。若正文已写入或LONGTEXT内容无法安全降为TEXT,0003 downgrade应明确阻止而非静默丢失。
  10. 当前正文检索使用参数化、转义后的LIKE满足现阶段数据规模;随着文档数量和正文体积增长,会出现大文本扫描性能风险。引入MySQL FULLTEXT或独立搜索服务前必须完成中文分词、相关性、权限过滤和迁移方案评审,不得在本阶段自行扩展。