# 设计:下载大文件功能(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 细节。