|
|
@@ -7,7 +7,7 @@
|
|
|
* **版本管理真空:** 覆盖式更新导致历史数据无法追溯,依赖关系容易崩溃。
|
|
|
|
|
|
## 2. 解决目标 (Objectives)
|
|
|
-构建一个“非侵入式”的轻量化中台,实现:
|
|
|
+构建一个"非侵入式"的轻量化中台,实现:
|
|
|
1. **自动归集:** 只要代码 Push 到 Git,系统自动提取该环节产出的数据。
|
|
|
2. **版本化:** 每次提交产生的成果都被唯一标记,互不覆盖。
|
|
|
3. **标准化:** 建立统一的数据目录结构。
|
|
|
@@ -15,7 +15,7 @@
|
|
|
|
|
|
## 3. 核心约定 (Social Contract / Agreements)
|
|
|
为了实现自动化,团队成员需要达成以下三点共识:
|
|
|
-1. **根目录配置文件:** 每个仓库根目录必须包含 `manifest.yaml`,声明哪些数据需要“上云”。
|
|
|
+1. **根目录配置文件:** 每个仓库根目录必须包含 `manifest.yaml`,声明哪些数据需要"上云"。
|
|
|
2. **结果文件落盘:** 代码运行后,结果必须产出到仓库目录内的指定位置(不支持读取仓库外的绝对路径)。
|
|
|
3. **必须执行 Git Push:** 只有 Push 动作会触发中台的数据采集。
|
|
|
|
|
|
@@ -26,17 +26,21 @@
|
|
|
|
|
|
### 4.2 存储方案
|
|
|
* **元数据存储:** 使用 **MySQL** 记录项目、环节、版本、文件索引。
|
|
|
-* **物理存储(二选一):**
|
|
|
- * **方案 A(推荐初始使用):** **服务器本地文件系统**。直接写入服务器磁盘(如 `/data/storage`),简单高效,适合文本和小文件。
|
|
|
- * **方案 B(进阶):** **对象存储 (OSS/MinIO)**。如果未来文件量大或需要可视化预览更方便,可无缝迁移至 MinIO。
|
|
|
-* **核心原则:** 数据库只存“路径”和“元数据”,不存文件内容。
|
|
|
+* **物理存储:** 使用 **阿里云 OSS + CDN** 存储文件内容。
|
|
|
+ * 文件上传到 OSS,通过 CDN 加速访问
|
|
|
+ * CDN 域名:`https://res-bj.cybertogether.net`
|
|
|
+ * 访问方式:`{CDN_URL}/{OSS_KEY}`
|
|
|
+* **核心原则:** 数据库只存"OSS Key"和"元数据",不存文件内容。
|
|
|
|
|
|
-### 4.3 数据获取机制 (核心变更)
|
|
|
+### 4.3 数据获取机制 (按需获取)
|
|
|
* **弃用 `git clone`:** 全量克隆效率低且浪费空间。
|
|
|
-* **采用 Gogs REST API:**
|
|
|
+* **弃用全量文件树:** 不再获取整个仓库的文件树,避免大仓库性能问题。
|
|
|
+* **采用按需获取策略:**
|
|
|
1. 通过 API 获取 `manifest.yaml` (Raw Content)。
|
|
|
- 2. 根据 Manifest 解析出文件列表。
|
|
|
- 3. 通过 API 获取文件 Git SHA,**仅下载发生变更的文件**。
|
|
|
+ 2. 解析 Manifest 获取 `outputs` 配置。
|
|
|
+ 3. **单文件配置**:直接调用 Contents API 获取该文件信息(包含 SHA)。
|
|
|
+ 4. **目录配置**:仅获取该目录下的文件树,递归遍历子目录。
|
|
|
+ 5. 根据 SHA 判断是否需要下载。
|
|
|
|
|
|
### 4.4 增量更新逻辑 (Smart Deduplication)
|
|
|
为了节省存储空间并提高效率,采用 **Git Blob SHA** 进行指纹比对。
|
|
|
@@ -54,31 +58,29 @@
|
|
|
* 查询某文件的历史版本时,通过 `relative_path` 向前查询 `data_files` 表即可。
|
|
|
|
|
|
### 4.5 存储结构可视化 (Visualization)
|
|
|
-最终在服务器磁盘(或 OSS Bucket)上的目录结构将是完全扁平且语义化的,通过 **Commit ID** 实现版本物理隔离。
|
|
|
+最终在 OSS Bucket 上的目录结构将是完全扁平且语义化的,通过 **Commit ID** 实现版本物理隔离。
|
|
|
|
|
|
-**目录树示例:**
|
|
|
+**OSS Key 结构:**
|
|
|
```text
|
|
|
-/opt/datahub/storage/
|
|
|
-├── topic_research/ <-- 项目名 (Project Name)
|
|
|
-│ ├── selection/ <-- 环节名 (Stage)
|
|
|
-│ │ ├── a1b2c3d4/ <-- [版本1] Commit ID (2023-10-01)
|
|
|
-│ │ │ ├── daily_report.csv
|
|
|
-│ │ │ └── output_images/
|
|
|
-│ │ │ ├── 001.png
|
|
|
-│ │ │ └── 002.png
|
|
|
-│ │ │
|
|
|
-│ │ └── e5f6g7h8/ <-- [版本2] Commit ID (2023-10-05)
|
|
|
-│ │ ├── daily_report.csv
|
|
|
-│ │ └── output_images/
|
|
|
-│ │ ├── 001.png
|
|
|
-│ │ └── 003.png
|
|
|
-│ │
|
|
|
-│ └── cleaning/ <-- 另一个环节
|
|
|
-│ └── ...
|
|
|
-└── ...
|
|
|
-```
|
|
|
-* **物理隔离:** 即使两个 versions 的 `daily_report.csv` 同名,它们也分别位于不同的 commit 文件夹下的,互不冲突。
|
|
|
-* **版本回溯:** 数据库中存储 `Commit ID -> /path/to/file` 的映射,想要回滚只需查库找到对应的文件夹即可。
|
|
|
+{prefix}/{project_name}/{stage}/{commit_id}/{relative_path}
|
|
|
+```
|
|
|
+
|
|
|
+**示例:**
|
|
|
+```text
|
|
|
+data_nexus/topic_research/selection/a1b2c3d4/daily_report.csv
|
|
|
+data_nexus/topic_research/selection/a1b2c3d4/output_images/001.png
|
|
|
+data_nexus/topic_research/selection/e5f6g7h8/daily_report.csv
|
|
|
+```
|
|
|
+
|
|
|
+**访问 URL:**
|
|
|
+```
|
|
|
+https://res-bj.cybertogether.net/data_nexus/topic_research/selection/a1b2c3d4/daily_report.csv
|
|
|
+```
|
|
|
+
|
|
|
+### 4.6 并发处理
|
|
|
+* **异步处理:** Webhook 请求立即返回,文件处理在后台异步执行。
|
|
|
+* **独立 Session:** 每个后台任务创建独立的数据库 Session,避免请求结束后 Session 被关闭的问题。
|
|
|
+* **多仓库并发:** 支持多个仓库同时推送 Webhook,各自独立处理。
|
|
|
|
|
|
## 5. 详细设计 (Detailed Design)
|
|
|
|
|
|
@@ -118,34 +120,41 @@ outputs:
|
|
|
```
|
|
|
|
|
|
### 5.2 数据库建模 (MySQL)
|
|
|
+
|
|
|
+**ID 策略:**
|
|
|
+* `projects` 和 `data_versions` 表使用 **ULID**(26 位字符串),便于数据迁移和分布式场景。
|
|
|
+* `data_files` 表使用自增 ID,因为文件记录量大且通常跟随 version 迁移。
|
|
|
+
|
|
|
```sql
|
|
|
CREATE TABLE `projects` (
|
|
|
- `id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `id` VARCHAR(26) PRIMARY KEY, -- ULID
|
|
|
`project_name` VARCHAR(100) NOT NULL UNIQUE,
|
|
|
`description` TEXT,
|
|
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
|
);
|
|
|
|
|
|
CREATE TABLE `data_versions` (
|
|
|
- `id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
- `project_id` INT,
|
|
|
+ `id` VARCHAR(26) PRIMARY KEY, -- ULID
|
|
|
+ `project_id` VARCHAR(26), -- 外键关联 projects.id
|
|
|
`stage` VARCHAR(50) NOT NULL,
|
|
|
- `commit_id` VARCHAR(64) NOT NULL, -- Git 的 Commit Hash
|
|
|
+ `commit_id` VARCHAR(64) NOT NULL, -- Git 的 Commit Hash
|
|
|
`author` VARCHAR(50),
|
|
|
- `manifest_snapshot` TEXT, -- 存储当时的 manifest.yaml 内容
|
|
|
+ `manifest_snapshot` TEXT, -- 存储当时的 manifest.yaml 内容
|
|
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
- INDEX(project_id, stage)
|
|
|
+ INDEX(project_id, stage),
|
|
|
+ FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
|
);
|
|
|
|
|
|
CREATE TABLE `data_files` (
|
|
|
- `id` INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
- `version_id` INT,
|
|
|
- `relative_path` VARCHAR(255), -- 原始相对路径
|
|
|
- `storage_path` VARCHAR(500), -- 在服务器上的绝对存储路径
|
|
|
+ `id` INT PRIMARY KEY AUTO_INCREMENT, -- 自增 ID
|
|
|
+ `version_id` VARCHAR(26), -- 外键关联 data_versions.id
|
|
|
+ `relative_path` VARCHAR(255), -- 原始相对路径
|
|
|
+ `storage_path` VARCHAR(500), -- OSS Key
|
|
|
`file_size` BIGINT,
|
|
|
- `file_type` VARCHAR(20), -- 扩展名
|
|
|
- `file_sha` VARCHAR(64), -- [新增] 文件的 Git Blob SHA,用于去重
|
|
|
+ `file_type` VARCHAR(20), -- 扩展名
|
|
|
+ `file_sha` VARCHAR(64), -- 文件的 Git Blob SHA,用于去重
|
|
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX(file_sha),
|
|
|
FOREIGN KEY (version_id) REFERENCES data_versions(id)
|
|
|
);
|
|
|
```
|
|
|
@@ -153,27 +162,95 @@ CREATE TABLE `data_files` (
|
|
|
### 5.3 中台后端逻辑流 (WorkFlow)
|
|
|
中台应用接收到 Gogs Webhook 请求后,执行以下步骤:
|
|
|
|
|
|
-1. **接收事件:** 获取仓库信息 (`owner`, `repo`) 和 `commit_id`。
|
|
|
-2. **获取清单 (API):**
|
|
|
+1. **接收事件:** 获取仓库信息 (`owner`, `repo`) 和 `commit_id`,立即返回响应。
|
|
|
+2. **后台处理:** 在独立的后台任务中执行以下操作:
|
|
|
+3. **获取清单 (API):**
|
|
|
* 调用 Gogs API: `GET /{owner}/{repo}/raw/{commit_id}/manifest.yaml`
|
|
|
* 若响应 404,则该次提交不包含数据,直接结束。
|
|
|
-3. **解析清单:** 读取 YAML,解析出 `project_name` 和 `stages` 配置。
|
|
|
-4. **获取文件树:** 调用 Gogs Tree API 获取该 commit 下所有文件及其 Blob SHA。
|
|
|
+4. **解析清单:** 读取 YAML,解析出 `project_name` 和 `stages` 配置。
|
|
|
5. **遍历 Stages:** 对每个 stage 执行以下操作:
|
|
|
- * 创建 `data_versions` 记录。
|
|
|
- * 遍历该 stage 的 `outputs` 配置,匹配文件树中的文件。
|
|
|
-6. **变更检测与处理:** 对每个匹配的文件:
|
|
|
+ * **幂等性检查:** 查询是否已存在相同 project + stage + commit_id 的记录,若存在则跳过。
|
|
|
+ * 创建 `data_versions` 记录(自动生成 ULID)。
|
|
|
+ * 遍历该 stage 的 `outputs` 配置。
|
|
|
+6. **按需获取文件信息:**
|
|
|
+ * **单文件**:调用 Contents API 获取文件信息(包含 SHA)。
|
|
|
+ * **目录**:调用 Contents API 递归获取目录下所有文件。
|
|
|
+7. **变更检测与处理:** 对每个匹配的文件:
|
|
|
* **查询历史:** 在 `data_files` 表中查找同一项目 + 同一 stage + 同一文件路径的**最新一条记录**。
|
|
|
* **对比 SHA:**
|
|
|
* **如果 SHA 相同:** 文件未变更,**跳过不记录**。
|
|
|
- * **如果 SHA 不同(或无历史):** 文件有变更,执行下载并在 `data_files` 表中新增记录。
|
|
|
-7. **文件下载与落盘:**
|
|
|
- * 仅当文件发生变更时,调用 Raw API 下载内容。
|
|
|
- * 将下载的数据流写入本地磁盘。
|
|
|
- * **路径隔离:** 严格按照 `/{project}/{stage}/{commit_id}/{filename}` 隔离。
|
|
|
+ * **如果 SHA 不同(或无历史):** 文件有变更,执行下载并上传到 OSS。
|
|
|
+8. **文件上传:**
|
|
|
+ * 调用 Raw API 下载文件内容。
|
|
|
+ * 上传到 OSS,Key 格式:`{prefix}/{project}/{stage}/{commit_id}/{relative_path}`
|
|
|
+ * 在 `data_files` 表中新增记录,`storage_path` 存储 OSS Key。
|
|
|
+
|
|
|
+## 6. API 接口设计
|
|
|
+
|
|
|
+### 6.1 Webhook 接口
|
|
|
+```
|
|
|
+POST /webhook
|
|
|
+```
|
|
|
+接收 Gogs Push 事件,支持 HMAC-SHA256 签名验证。
|
|
|
+
|
|
|
+### 6.2 项目接口
|
|
|
+```
|
|
|
+GET /projects # 列出所有项目
|
|
|
+GET /projects/{project_id} # 获取单个项目(ID 为 ULID)
|
|
|
+GET /projects/name/{name} # 按名称获取项目
|
|
|
+```
|
|
|
+
|
|
|
+### 6.3 版本接口
|
|
|
+```
|
|
|
+GET /projects/{project_id}/versions?stage=xxx # 列出项目版本,可按 stage 过滤
|
|
|
+GET /versions/{version_id} # 获取单个版本(ID 为 ULID)
|
|
|
+GET /versions/{version_id}/files?flat=true # 获取版本文件(树形/扁平)
|
|
|
+```
|
|
|
+
|
|
|
+### 6.4 文件接口
|
|
|
+```
|
|
|
+GET /files/{file_id} # 获取文件元数据(ID 为自增整数)
|
|
|
+GET /files/{file_id}/url # 获取文件 CDN URL
|
|
|
+GET /files/{file_id}/content # 重定向到 CDN URL 下载
|
|
|
+```
|
|
|
+
|
|
|
+## 7. 配置项
|
|
|
+
|
|
|
+### 7.1 环境变量
|
|
|
+```bash
|
|
|
+# 数据库配置
|
|
|
+DB_HOST=localhost
|
|
|
+DB_PORT=3306
|
|
|
+DB_USER=root
|
|
|
+DB_PASSWORD=xxx
|
|
|
+DB_NAME=data_nexus
|
|
|
+
|
|
|
+# Gogs 配置
|
|
|
+GOGS_URL=https://git.example.com
|
|
|
+GOGS_TOKEN=xxx
|
|
|
+GOGS_SECRET= # Webhook 签名密钥(可选)
|
|
|
+
|
|
|
+# OSS 配置
|
|
|
+OSS_ACCESS_KEY_ID=xxx
|
|
|
+OSS_ACCESS_KEY_SECRET=xxx
|
|
|
+OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
|
|
|
+OSS_BUCKET_NAME=xxx
|
|
|
+OSS_PREFIX=data_nexus
|
|
|
+OSS_CDN_URL=https://res-bj.cybertogether.net
|
|
|
+```
|
|
|
+
|
|
|
+## 8. 约定细节补充 (Constraints)
|
|
|
+* **幂等性:** 同一 Commit ID + Stage 若重复触发,系统会检查数据库,若已存在则跳过。
|
|
|
+* **安全性:** 使用 Gogs Token 进行 API 认证,支持 Webhook 签名验证。
|
|
|
+* **大文件:** 建议单文件大小控制在 500MB 以内。OSS 支持大文件,但下载时间会较长。
|
|
|
|
|
|
+## 9. 技术栈
|
|
|
|
|
|
-## 6. 约定细节补充 (Constraints)
|
|
|
-* **文件冲突:** 同一 Commit ID 若重复触发,系统应先检查数据库,若已存在则跳过,防止重复占用空间。
|
|
|
-* **安全性:** 中台服务器需要配置好访问 Gogs 的 SSH Key,以便有权限拉取私有仓库代码。
|
|
|
-* **大文件:** 考虑到仅使用 MySQL,单文件大小建议控制在 500MB 以内。如果未来有超大文件(如几个GB),建议再考虑挂载 NAS。
|
|
|
+| 组件 | 技术选型 |
|
|
|
+|------|----------|
|
|
|
+| Web 框架 | FastAPI |
|
|
|
+| 数据库 | MySQL + SQLAlchemy |
|
|
|
+| HTTP 客户端 | httpx (异步) |
|
|
|
+| 对象存储 | 阿里云 OSS |
|
|
|
+| ID 生成 | ULID (python-ulid) |
|
|
|
+| 配置管理 | python-dotenv |
|