新增电子试验主文件目录与文件归档

This commit is contained in:
Cheng Zhou
2026-05-28 10:54:13 +08:00
parent 31fcb7e6f2
commit fa961f1391
20 changed files with 2226 additions and 10 deletions
+151
View File
@@ -0,0 +1,151 @@
# eTMF Module Design
## 背景
当前系统已有文件版本管理能力,包括 `documents``document_versions`、文件上传下载、分发、回执、权限和审计。`/etmf` 路由目前是占位页,并已复用 `documents:read` 权限。
eTMF 第一阶段需要同时满足两个目标:
- 合规归档视图:按 TMF 标准目录检查项目级和中心级文件归档状态。
- 文件管理入口:项目成员可以按目录上传、查找、下载、维护版本。
## 推荐方案
采用“TMF 目录树模板 + 文档元数据挂载”模型。前端呈现为目录结构,但后端不把 eTMF 当作真实文件夹系统,而是用目录节点表达归档标准,用现有文档实体承载文件和版本。
该方案保留文件夹式使用体验,同时支持合规检查、缺失项统计、中心维度对比和后续报表扩展。
## 核心模型
### eTMF 目录节点
新增 `etmf_nodes` 表,表达项目内 eTMF 目录树。
建议字段:
- `id`
- `study_id`
- `parent_id`
- `code`
- `name`
- `description`
- `scope_type`: `GLOBAL``SITE`
- `required`
- `expected_doc_type`
- `sort_order`
- `is_active`
- `created_at`
- `updated_at`
约束:
- 同一项目、同一父节点下 `code` 唯一。
- 节点软停用,不物理删除,避免历史文档失去归档上下文。
- 第一阶段不做自定义模板版本管理,只提供项目内节点树。
### 文档挂载
复用现有 `documents` 表,新增 `etmf_node_id` 外键。
规则:
- `documents.etmf_node_id` 为空表示普通文件版本管理文档。
- `documents.etmf_node_id` 非空表示该文档归档到 eTMF 节点。
- 项目级节点对应 `documents.site_id = null`
- 中心级节点对应 `documents.site_id` 有值。
第一阶段不新增独立 eTMF 文件表,避免重复存储文件元数据和版本逻辑。
## 状态计算
eTMF 状态优先通过查询计算,不在第一阶段持久化冗余状态。
节点状态:
- `NOT_REQUIRED`: 非必需节点且无文档。
- `MISSING`: 必需节点没有匹配文档。
- `UPLOADED`: 有文档但没有生效版本。
- `EFFECTIVE`: 至少一个匹配文档存在当前生效版本。
- `INACTIVE`: 节点停用。
中心级节点按中心分别计算状态。项目级节点只计算项目维度。
## 后端接口
新增 eTMF API,挂载在现有 v1 router 下。
建议接口:
- `GET /api/v1/etmf/tree?study_id=...`
返回节点树、每个节点汇总状态和文档数量。
- `GET /api/v1/etmf/nodes/{node_id}/documents?site_id=...`
返回节点下文档列表,复用 `DocumentSummary`
- `POST /api/v1/etmf/nodes`
创建节点。
- `PATCH /api/v1/etmf/nodes/{node_id}`
更新节点名称、排序、必需性、适用范围。
- `POST /api/v1/etmf/nodes/{node_id}/documents`
在节点下创建文档,本质调用现有 `create_document`,自动带入 `etmf_node_id`
第一阶段不做节点移动和批量导入,除非后续确认需要。
## 前端体验
替换当前 `EtmfPlaceholder.vue`
页面结构:
- 顶部工具栏:中心筛选、状态筛选、刷新、新增目录、新增文档。
- 左侧:eTMF 目录树,显示节点名称、编码、状态标记、缺失提示。
- 中间:当前节点文档列表,展示标题、类型、范围、中心、当前版本、更新时间、操作。
- 右侧:节点详情,展示必需性、适用范围、完成状态、缺失说明、中心完成度摘要。
交互规则:
- 点击目录节点加载该节点文档。
- 创建文档时默认带入当前节点和当前中心筛选。
- 文档详情和版本维护复用现有文档详情页。
- 中心级节点在选择中心后展示该中心归档状态;未选中心时展示中心完成度汇总。
## 权限和审计
第一阶段复用现有文档权限:
- 查看:`documents:read`
- 新建文档或目录:`documents:create`
- 更新文档版本或节点:`documents:update`
- 删除或停用:`documents:delete`
审计:
- 文档创建、版本上传、下载、删除继续复用现有文档审计。
- 节点创建、更新、停用新增 `ETMF_NODE_CREATED``ETMF_NODE_UPDATED``ETMF_NODE_DISABLED` 审计动作。
## 测试策略
后端:
- 节点树接口按项目隔离。
- 必需节点缺失状态计算正确。
- 中心级节点按 `site_id` 计算状态。
- CRA 只能看到自身中心范围内的中心级文件。
- 节点下创建文档会写入 `etmf_node_id`
前端:
- `/etmf` 不再展示占位页。
- 节点树加载、节点选择、中心筛选、状态展示。
- 新建文档时携带当前节点。
- 路由权限继续使用 `documents:read`
## 取舍
第一阶段明确不做:
- 电子签名。
- 复杂审批流。
- 模板版本发布。
- 物理文件夹移动。
- 批量导入 TMF 标准目录。
这些能力可以在基础目录树和文档挂载稳定后追加。
@@ -0,0 +1,386 @@
# eTMF Module Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build an eTMF module that combines a compliant TMF directory tree with reusable document version management.
**Architecture:** Add eTMF directory nodes as metadata, then mount existing `documents` records onto those nodes with `etmf_node_id`. Keep file storage, versions, permissions, downloads, distribution, and audit behavior in the existing document module.
**Tech Stack:** FastAPI, SQLAlchemy async ORM, Alembic, PostgreSQL, Vue 3, Element Plus, Vitest, Pytest.
---
## Implementation Notes
The repository instruction says not to plan or execute git commits unless explicitly requested. This plan intentionally omits commit steps.
Follow TDD for each backend behavior before implementation. Keep the first release small: no e-signature, no workflow redesign, no template versioning, no bulk import.
### Task 1: Backend eTMF Node Model
**Files:**
- Create: `backend/app/models/etmf.py`
- Create: `backend/app/schemas/etmf.py`
- Modify: `backend/app/db/base.py`
- Create: `backend/alembic/versions/20260527_07_add_etmf_nodes.py`
- Test: `backend/tests/test_etmf_nodes.py`
**Step 1: Write failing model/import test**
Add a test that imports `EtmfNode` through `app.db.base` metadata and verifies the table name is registered.
Run:
```bash
cd backend
pytest tests/test_etmf_nodes.py -q
```
Expected: fail because `app.models.etmf` does not exist.
**Step 2: Add model**
Create `EtmfNode` with these columns:
- `id`
- `study_id`
- `parent_id`
- `code`
- `name`
- `description`
- `scope_type`
- `required`
- `expected_doc_type`
- `sort_order`
- `is_active`
- `created_at`
- `updated_at`
Use existing enum values from document scope where practical. Keep node-specific logic out of the model.
**Step 3: Add schemas**
Create request and response schemas:
- `EtmfNodeCreate`
- `EtmfNodeUpdate`
- `EtmfNodeRead`
- `EtmfTreeNode`
- `EtmfNodeStatusSummary`
**Step 4: Add migration**
Create `etmf_nodes` with:
- Foreign key to `studies.id`
- Self foreign key `parent_id`
- Unique constraint on `(study_id, parent_id, code)`
- Indexes on `study_id`, `parent_id`, `scope_type`, `is_active`
**Step 5: Verify**
Run:
```bash
cd backend
pytest tests/test_etmf_nodes.py -q
```
Expected: pass.
### Task 2: Link Documents To eTMF Nodes
**Files:**
- Modify: `backend/app/models/document.py`
- Modify: `backend/app/schemas/document.py`
- Modify: `backend/app/crud/document.py`
- Modify: `backend/app/services/document_service.py`
- Modify: `frontend/src/types/documents.ts`
- Modify: `backend/alembic/versions/20260527_07_add_etmf_nodes.py`
- Test: `backend/tests/test_etmf_nodes.py`
**Step 1: Write failing service test**
Test that creating a document with `etmf_node_id` persists the relationship and list/detail responses include it.
Expected: fail because schema and model do not include `etmf_node_id`.
**Step 2: Add model column**
Add nullable `Document.etmf_node_id` with foreign key to `etmf_nodes.id`.
**Step 3: Add schema fields**
Add `etmf_node_id` to:
- `DocumentCreate`
- `DocumentUpdate`
- `DocumentSummary`
- `DocumentDetail`
**Step 4: Validate node scope**
In `document_service.create_document`, if `etmf_node_id` is provided:
- Verify node exists.
- Verify node belongs to the same study.
- If node scope is `SITE`, require `site_id`.
- If node scope is `GLOBAL`, normalize `site_id` to null unless the product decision changes.
**Step 5: Verify**
Run:
```bash
cd backend
pytest tests/test_etmf_nodes.py -q
```
Expected: pass.
### Task 3: eTMF Service And Tree API
**Files:**
- Create: `backend/app/crud/etmf.py`
- Create: `backend/app/services/etmf_service.py`
- Create: `backend/app/api/v1/etmf.py`
- Modify: `backend/app/api/v1/router.py`
- Test: `backend/tests/test_etmf_api.py`
**Step 1: Write failing API tests**
Cover:
- `GET /api/v1/etmf/tree?study_id=...`
- Required empty node returns `MISSING`.
- Node with document but no effective version returns `UPLOADED`.
- Node with current effective version returns `EFFECTIVE`.
- Site-scoped node can return per-site status.
Expected: fail because API does not exist.
**Step 2: Implement CRUD**
Add small CRUD functions:
- `get_node`
- `list_nodes_by_study`
- `create_node`
- `update_node`
Avoid broad generic repository abstractions.
**Step 3: Implement service**
Add tree assembly and status calculation:
- Build parent-child tree in memory.
- Query documents for target study and group by `etmf_node_id`.
- Calculate node status from required flag and document effective version presence.
- Keep status calculation deterministic and isolated for unit tests.
**Step 4: Implement routes**
Add:
- `GET /tree`
- `GET /nodes/{node_id}/documents`
- `POST /nodes`
- `PATCH /nodes/{node_id}`
- `POST /nodes/{node_id}/documents`
Use existing project permission checks through the document permission model.
**Step 5: Verify**
Run:
```bash
cd backend
pytest tests/test_etmf_api.py -q
```
Expected: pass.
### Task 4: Frontend API And Types
**Files:**
- Create: `frontend/src/types/etmf.ts`
- Create: `frontend/src/api/etmf.ts`
- Test: `frontend/src/api/etmf.test.ts`
**Step 1: Write failing API test**
Test that each frontend API helper calls the expected URL and HTTP method.
Expected: fail because `src/api/etmf.ts` does not exist.
**Step 2: Add types**
Add types matching backend schemas:
- `EtmfNodeStatus`
- `EtmfTreeNode`
- `EtmfNodeCreatePayload`
- `EtmfNodeUpdatePayload`
**Step 3: Add API helpers**
Add:
- `fetchEtmfTree`
- `fetchEtmfNodeDocuments`
- `createEtmfNode`
- `updateEtmfNode`
- `createEtmfDocument`
**Step 4: Verify**
Run:
```bash
cd frontend
npm test -- src/api/etmf.test.ts
```
Expected: pass.
### Task 5: eTMF Page
**Files:**
- Replace: `frontend/src/views/ia/EtmfPlaceholder.vue`
- Modify: `frontend/src/locales/zh-CN.ts`
- Test: `frontend/src/views/ia/EtmfPlaceholder.test.ts`
**Step 1: Write failing page test**
Test that `/etmf` page:
- Renders tree panel.
- Loads eTMF tree for current study.
- Selecting a node loads its documents.
- Shows missing/effective status labels.
Expected: fail because the page is still a placeholder.
**Step 2: Build layout**
Use existing CTMS shell classes:
- Top action bar.
- Left tree panel.
- Main document table.
- Right node detail panel.
Do not introduce a marketing-style page or nested card layout.
**Step 3: Add interactions**
Implement:
- Center filter.
- Node selection.
- Refresh.
- Create node dialog.
- Create document dialog that posts to eTMF node document API.
Reuse existing display helpers and document enums.
**Step 4: Verify**
Run:
```bash
cd frontend
npm test -- src/views/ia/EtmfPlaceholder.test.ts
```
Expected: pass.
### Task 6: Route And Permission Regression
**Files:**
- Modify: `frontend/src/utils/projectRoutePermissions.ts`
- Modify: `frontend/src/utils/projectRoutePermissions.test.ts`
- Modify: `backend/tests/test_api_permissions.py`
**Step 1: Write failing regression tests**
Ensure:
- `/etmf` still requires `documents:read`.
- eTMF write endpoints are covered by document permissions.
- Existing document permission tests remain stable.
**Step 2: Implement minimal permission wiring**
Keep first-stage eTMF permissions mapped to existing document operation keys.
**Step 3: Verify**
Run:
```bash
cd backend
pytest tests/test_api_permissions.py -q
cd ../frontend
npm test -- src/utils/projectRoutePermissions.test.ts
```
Expected: pass.
### Task 7: End-To-End Verification
**Files:**
- No source changes expected unless verification finds a bug.
**Step 1: Run backend focused tests**
```bash
cd backend
pytest tests/test_etmf_nodes.py tests/test_etmf_api.py tests/test_api_permissions.py -q
```
Expected: pass.
**Step 2: Run frontend focused tests**
```bash
cd frontend
npm test -- src/api/etmf.test.ts src/views/ia/EtmfPlaceholder.test.ts src/utils/projectRoutePermissions.test.ts
```
Expected: pass.
**Step 3: Run UI contract check if available**
```bash
cd frontend
npm run verify-ui-contract
```
Expected: pass.
**Step 4: Manual browser check**
Start the dev server and open `/etmf` with an active study:
```bash
cd frontend
npm run dev
```
Expected:
- eTMF page is not blank.
- Directory tree is visible.
- Selecting a node updates the document list.
- Center filter does not break layout.
- Text does not overlap at desktop and mobile widths.