新增电子试验主文件目录与文件归档
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user