init
This commit is contained in:
266
docs/2026-03-27-12-09-设计-下载大文件功能.md
Normal file
266
docs/2026-03-27-12-09-设计-下载大文件功能.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 设计:下载大文件功能(v1)
|
||||
|
||||
- 文档类别:设计(系统设计)
|
||||
- 创建时间:2026-03-27 12:09 (Asia/Shanghai)
|
||||
- 适用项目:ToolsShow(NestJS + Prisma + GitLab Generic Package)
|
||||
- 关联模块:`access`、`downloads`、`gitlab-storage`
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
当前下载链路(`POST /tools/:id/launch` -> `GET /downloads/:ticket`)可以完成普通文件下载,但在大文件场景存在明显短板:
|
||||
|
||||
1. 现有 ticket 为“一次性消费”(`consumedAt` 写入后不可重用),网络中断后无法原 ticket 续传。
|
||||
2. `GET /downloads/:ticket` 仅做整文件流式透传,没有 `Range` / `206 Partial Content`,无法断点续传。
|
||||
3. 对下载过程缺少阶段化记录(开始、部分成功、失败重试),难以统计真实下载质量。
|
||||
4. 大文件下载失败时用户体验较差(必须回到前端重新触发 launch)。
|
||||
|
||||
## 2. 目标与非目标
|
||||
|
||||
### 2.1 目标
|
||||
|
||||
1. 支持标准 HTTP 断点续传(`Range`、`Accept-Ranges`、`Content-Range`、`206`)。
|
||||
2. 网络中断后允许在有效期内继续下载,不要求重新 launch。
|
||||
3. 在不改变“统一 launch 入口”前提下完成兼容升级。
|
||||
4. 增加大文件下载可观测性(失败率、平均耗时、重试次数、完成率)。
|
||||
5. 兼容两种存储后端:GitLab 远端包与本地文件回退存储。
|
||||
|
||||
### 2.2 非目标(本期不做)
|
||||
|
||||
1. P2P/BT 分发。
|
||||
2. CDN 回源策略编排。
|
||||
3. 客户端多线程分片加速协议(先支持标准浏览器与下载器)。
|
||||
|
||||
## 3. 设计原则
|
||||
|
||||
1. **兼容优先**:旧接口可保留短期兼容,前端可灰度切换。
|
||||
2. **安全优先**:令牌短期有效、可撤销、与工具/制品强绑定。
|
||||
3. **可恢复优先**:会话级下载权限 > 一次性 ticket。
|
||||
4. **可运维优先**:必须可追踪失败原因与瓶颈位置(应用层/存储层/网络层)。
|
||||
|
||||
## 4. 总体方案
|
||||
|
||||
采用“**下载会话(Download Session)+ Range 流式传输**”替代“一次性 ticket + 全量下载”。
|
||||
|
||||
### 4.1 核心变化
|
||||
|
||||
1. 下载模式 launch 不再返回一次性 ticket,而是返回可续传会话 token。
|
||||
2. 新下载接口支持 `HEAD` 与 `GET + Range`。
|
||||
3. 会话在有效期内可多次请求同一文件不同字节区间。
|
||||
4. 下载记录改为“会话聚合 + 分段记录”,用于分析中断与重试。
|
||||
|
||||
### 4.2 兼容策略
|
||||
|
||||
1. 保留 `GET /downloads/:ticket`(旧)1-2 个版本周期。
|
||||
2. 新增 `GET /downloads/sessions/:token/file`(新)。
|
||||
3. 前端先读 launch 返回字段,若存在 `sessionToken` 则走新链路,否则走旧链路。
|
||||
|
||||
## 5. 数据模型设计
|
||||
|
||||
> 以下为设计层面的 Prisma 结构草案,具体字段可在实现阶段微调。
|
||||
|
||||
### 5.1 新增表:`download_sessions`
|
||||
|
||||
- `id`:String (UUID)
|
||||
- `sessionToken`:String (Unique)
|
||||
- `toolId`:String
|
||||
- `artifactId`:String
|
||||
- `channel`:String?
|
||||
- `clientVersion`:String?
|
||||
- `requestIp`:String?
|
||||
- `userAgent`:String?
|
||||
- `expiresAt`:DateTime
|
||||
- `lastAccessAt`:DateTime
|
||||
- `completedAt`:DateTime?
|
||||
- `revokedAt`:DateTime?
|
||||
- `createdAt`:DateTime
|
||||
|
||||
索引建议:
|
||||
- `(sessionToken unique)`
|
||||
- `(expiresAt)`
|
||||
- `(artifactId, expiresAt)`
|
||||
|
||||
### 5.2 新增表:`download_session_chunks`(可选但建议)
|
||||
|
||||
- `id`:Int (auto increment)
|
||||
- `sessionId`:String
|
||||
- `rangeStart`:BigInt
|
||||
- `rangeEnd`:BigInt
|
||||
- `bytesSent`:BigInt
|
||||
- `status`:`success | failed | cancelled`
|
||||
- `errorMessage`:String?
|
||||
- `durationMs`:Int?
|
||||
- `createdAt`:DateTime
|
||||
|
||||
说明:用于分析大文件下载过程中的断点位置、失败区间、重试质量。
|
||||
|
||||
## 6. API 设计
|
||||
|
||||
Base path: `/api/v1`
|
||||
|
||||
### 6.1 Launch(下载模式)响应升级
|
||||
|
||||
`POST /tools/:id/launch`
|
||||
|
||||
下载模式响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "download",
|
||||
"sessionToken": "dl_sess_xxx",
|
||||
"expiresInSec": 3600,
|
||||
"actionUrl": "/api/v1/downloads/sessions/dl_sess_xxx/file",
|
||||
"resumeSupported": true,
|
||||
"file": {
|
||||
"name": "tool-v2.1.0.zip",
|
||||
"size": 2147483648,
|
||||
"sha256": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 文件元信息探测
|
||||
|
||||
`HEAD /downloads/sessions/:token/file`
|
||||
|
||||
返回:
|
||||
- `Accept-Ranges: bytes`
|
||||
- `Content-Length`
|
||||
- `ETag`(建议使用 artifact sha256)
|
||||
- `Content-Disposition`
|
||||
|
||||
### 6.3 下载文件(支持 Range)
|
||||
|
||||
`GET /downloads/sessions/:token/file`
|
||||
|
||||
请求头:
|
||||
- 可选 `Range: bytes=0-1048575`
|
||||
|
||||
响应:
|
||||
- 无 Range:`200` + 全量流
|
||||
- 有效 Range:`206` + 指定区间流
|
||||
- 非法 Range:`416`
|
||||
|
||||
关键响应头:
|
||||
- `Accept-Ranges: bytes`
|
||||
- `Content-Range`
|
||||
- `Content-Length`
|
||||
- `Content-Type`
|
||||
- `Content-Disposition`
|
||||
- `ETag`
|
||||
|
||||
### 6.4 会话失效
|
||||
|
||||
- 过期或撤销:`410 Gone`
|
||||
- 无效 token:`404` 或 `401`(按安全策略统一)
|
||||
|
||||
## 7. 服务端实现设计
|
||||
|
||||
## 7.1 AccessService 改造
|
||||
|
||||
文件:`server/src/modules/access/access.service.ts`
|
||||
|
||||
1. 下载模式不再创建一次性 `downloadTicket`,改为创建 `downloadSession`。
|
||||
2. 默认会话 TTL 建议 1 小时(可配置 `DOWNLOAD_SESSION_TTL_SEC`)。
|
||||
3. 返回 `sessionToken + actionUrl + file meta`。
|
||||
|
||||
## 7.2 DownloadsController 改造
|
||||
|
||||
文件:`server/src/modules/downloads/downloads.controller.ts`
|
||||
|
||||
新增路由:
|
||||
- `HEAD /downloads/sessions/:token/file`
|
||||
- `GET /downloads/sessions/:token/file`
|
||||
|
||||
保留旧路由:
|
||||
- `GET /downloads/:ticket`(兼容期)
|
||||
|
||||
## 7.3 DownloadsService 改造
|
||||
|
||||
文件:`server/src/modules/downloads/downloads.service.ts`
|
||||
|
||||
新增能力:
|
||||
1. 解析并校验 Range。
|
||||
2. 校验会话有效性、工具状态、制品状态。
|
||||
3. 根据 Range 组装响应头并返回 `200/206/416`。
|
||||
4. 在响应关闭/中断时记录 chunk 结果。
|
||||
5. 当客户端拿到完整文件后标记 `completedAt`(可通过全量下载成功或累计字节判定)。
|
||||
|
||||
## 7.4 GitlabStorageService 改造
|
||||
|
||||
文件:`server/src/modules/gitlab-storage/gitlab-storage.service.ts`
|
||||
|
||||
新增方法建议:
|
||||
- `getArtifactStream(artifact, range?)`
|
||||
- `headArtifact(artifact)`
|
||||
|
||||
实现要点:
|
||||
1. 远端 GitLab 下载请求透传 `Range` 头。
|
||||
2. 若 GitLab 返回 `206`,直接桥接状态码与头。
|
||||
3. 若远端不支持 Range,则回退为 `200` 全量(并在响应中标记 `resumeSupported=false`)。
|
||||
4. 本地文件场景使用 `createReadStream(path, { start, end })`。
|
||||
|
||||
## 8. 安全与风控
|
||||
|
||||
1. `sessionToken` 使用高熵随机串;数据库只保存 hash(推荐)。
|
||||
2. 会话与 `toolId/artifactId` 强绑定,防止跨资源复用。
|
||||
3. 限制并发分段请求数(例如单会话最多 4 并发)。
|
||||
4. 单 IP / 单工具限流,防止恶意刷取带宽。
|
||||
5. 所有下载响应增加 `X-Content-Type-Options: nosniff`。
|
||||
|
||||
## 9. 观测指标
|
||||
|
||||
1. `download_session_started_total`
|
||||
2. `download_chunk_success_total`
|
||||
3. `download_chunk_failed_total`
|
||||
4. `download_session_completed_total`
|
||||
5. `download_resume_ratio`(续传请求占比)
|
||||
6. `download_5xx_ratio`
|
||||
7. `p95_chunk_duration_ms`
|
||||
|
||||
日志字段建议:`traceId`、`sessionId`、`artifactId`、`rangeStart`、`rangeEnd`、`bytesSent`、`status`、`errorCode`。
|
||||
|
||||
## 10. 迁移与发布计划
|
||||
|
||||
### Phase 1(后端可用)
|
||||
|
||||
1. 落库 `download_sessions`。
|
||||
2. 新增会话下载接口 + Range 支持。
|
||||
3. Access 返回新字段。
|
||||
4. 保留旧 ticket 接口。
|
||||
|
||||
### Phase 2(前端切换)
|
||||
|
||||
1. 前端优先走 `sessionToken` 新链路。
|
||||
2. 引入失败重试与断点续传提示。
|
||||
3. 观察 1 周核心指标。
|
||||
|
||||
### Phase 3(收敛)
|
||||
|
||||
1. 宣布下线旧 ticket 下载接口。
|
||||
2. 清理旧表和兼容逻辑(按版本策略执行)。
|
||||
|
||||
## 11. 风险与应对
|
||||
|
||||
1. **GitLab Range 兼容性不一致**:先做能力探测(HEAD/小范围 GET),不支持时降级。
|
||||
2. **高并发导致服务端带宽占满**:增加会话并发限制 + 网关限流。
|
||||
3. **大文件长连接导致 Node 资源占用**:严格使用流式处理,避免读入内存;设置连接超时与中断清理。
|
||||
4. **统计口径变化**:区分“会话完成”与“分段成功”,避免误解下载成功率。
|
||||
|
||||
## 12. 验收标准
|
||||
|
||||
1. 2GB 文件可在中断后 5 分钟内基于同一 `sessionToken` 成功续传。
|
||||
2. `Range` 请求返回符合 RFC 7233 的响应码与头部。
|
||||
3. 下载中断、失败、成功均有可追踪日志。
|
||||
4. 旧版前端不改动时仍可通过旧 ticket 接口下载。
|
||||
|
||||
## 13. 对应当前代码的最小改造清单
|
||||
|
||||
1. `access.service.ts`:创建并返回 `downloadSession`。
|
||||
2. `downloads.controller.ts`:新增 `HEAD/GET /downloads/sessions/:token/file`。
|
||||
3. `downloads.service.ts`:实现会话校验、Range 解析、206/416 响应、chunk 记录。
|
||||
4. `gitlab-storage.service.ts`:支持 Range 透传与本地分段读取。
|
||||
5. `prisma/schema.prisma`:新增会话与分段记录模型。
|
||||
|
||||
---
|
||||
|
||||
本设计文档用于“下载大文件功能”研发基线,可在进入实现阶段前补充更细的 DTO、错误码扩展和数据库 migration 细节。
|
||||
Reference in New Issue
Block a user