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 细节。
|
||||
223
docs/TOOLSHOW_ER.drawio
Normal file
223
docs/TOOLSHOW_ER.drawio
Normal file
@@ -0,0 +1,223 @@
|
||||
<mxfile host="app.diagrams.net" modified="2026-03-27T00:00:00.000Z" agent="Codex" version="24.7.17">
|
||||
<diagram id="toolsshow-er" name="ToolsShow-ER">
|
||||
<mxGraphModel dx="1800" dy="980" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="4200" pageHeight="2200" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
|
||||
<mxCell id="e_categories" value="categories" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="80" y="220" width="150" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_tools" value="tools" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="360" y="220" width="170" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_artifacts" value="tool_artifacts" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="700" y="220" width="190" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_admin_users" value="admin_users" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1040" y="220" width="170" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_hot_keywords" value="hot_keywords" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="80" y="520" width="170" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_tags" value="tags" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="360" y="520" width="150" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_tool_tags" value="tool_tags" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="560" y="520" width="170" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_tickets" value="download_tickets" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="760" y="520" width="210" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_records" value="download_records" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1010" y="520" width="210" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="e_features" value="tool_features" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="360" y="820" width="170" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_open_records" value="open_records" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="700" y="820" width="190" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_audit_logs" value="admin_audit_logs" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1020" y="820" width="220" height="56" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="r_cat_tools" value="belongs_to_category" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="260" y="226" width="80" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_tools_artifacts" value="has_artifact" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="580" y="226" width="90" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_tools_latest" value="latest_artifact_ref" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="560" y="120" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_admin_artifacts" value="uploaded_by" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="930" y="226" width="90" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="r_tools_tooltags" value="tool_ref" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="460" y="370" width="80" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_tags_tooltags" value="tag_ref" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="520" y="526" width="80" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_tools_features" value="has_feature" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="400" y="710" width="90" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="r_tools_tickets" value="creates_ticket" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="620" y="406" width="100" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_artifacts_tickets" value="targets_artifact" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="800" y="406" width="110" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="r_tools_records" value="download_of_tool" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="820" y="620" width="110" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_artifacts_records" value="download_of_artifact" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="950" y="406" width="130" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_tools_open" value="open_event" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="620" y="716" width="90" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="r_admin_audit" value="operates" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;" vertex="1" parent="1">
|
||||
<mxGeometry x="1080" y="680" width="90" height="44" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="a_categories_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="130" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_tools_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="360" y="130" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_artifacts_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="700" y="130" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_admin_users_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="1040" y="130" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="a_hot_keywords_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="80" y="600" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_tags_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="600" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_tool_tags_pk" value="tool_id + tag_id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="560" y="600" width="150" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_tickets_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="760" y="600" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_records_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="1010" y="600" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="a_features_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="360" y="900" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_open_records_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="700" y="900" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="a_audit_logs_id" value="id (PK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#333333;fontSize=11;fontStyle=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="1020" y="900" width="90" height="36" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="l1" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_categories" target="r_cat_tools"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l2" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_cat_tools" target="e_tools"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l3" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tools" target="r_tools_artifacts"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l4" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tools_artifacts" target="e_artifacts"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l5" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tools" target="r_tools_latest"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l6" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tools_latest" target="e_artifacts"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l7" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_admin_users" target="r_admin_artifacts"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l8" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_admin_artifacts" target="e_artifacts"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l9" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tools" target="r_tools_tooltags"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l10" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tools_tooltags" target="e_tool_tags"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l11" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tags" target="r_tags_tooltags"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l12" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tags_tooltags" target="e_tool_tags"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l13" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tools" target="r_tools_features"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l14" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tools_features" target="e_features"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l15" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tools" target="r_tools_tickets"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l16" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tools_tickets" target="e_tickets"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l17" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_artifacts" target="r_artifacts_tickets"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l18" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_artifacts_tickets" target="e_tickets"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l19" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tools" target="r_tools_records"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l20" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tools_records" target="e_records"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l21" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_artifacts" target="r_artifacts_records"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l22" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_artifacts_records" target="e_records"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l23" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_tools" target="r_tools_open"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l24" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_tools_open" target="e_open_records"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="l25" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="e_admin_users" target="r_admin_audit"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="l26" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1.5;" edge="1" parent="1" source="r_admin_audit" target="e_audit_logs"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="la1" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_categories" target="a_categories_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la2" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_tools" target="a_tools_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la3" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_artifacts" target="a_artifacts_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la4" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_admin_users" target="a_admin_users_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la5" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_hot_keywords" target="a_hot_keywords_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la6" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_tags" target="a_tags_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la7" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_tool_tags" target="a_tool_tags_pk"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la8" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_tickets" target="a_tickets_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la9" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_records" target="a_records_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la10" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_features" target="a_features_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la11" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_open_records" target="a_open_records_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="la12" style="endArrow=none;html=1;strokeColor=#333333;strokeWidth=1;" edge="1" parent="1" source="e_audit_logs" target="a_audit_logs_id"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c1" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="232" y="238" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c2" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="345" y="238" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c3" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="535" y="238" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c4" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="675" y="238" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c5" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="545" y="148" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c6" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="683" y="148" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c7" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="1018" y="238" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c8" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="915" y="238" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c9" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="448" y="350" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c10" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="544" y="448" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c11" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="510" y="538" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c12" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="606" y="538" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c13" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="428" y="690" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c14" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="428" y="760" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c15" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="598" y="404" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c16" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="736" y="468" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c17" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="776" y="392" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c18" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="898" y="468" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c19" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="808" y="608" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c20" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="940" y="608" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c21" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="938" y="392" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c22" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="1086" y="468" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c23" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="598" y="704" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c24" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="728" y="774" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="c25" value="1" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="1080" y="664" width="20" height="20" as="geometry"/></mxCell>
|
||||
<mxCell id="c26" value="n" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"><mxGeometry x="1160" y="772" width="20" height="20" as="geometry"/></mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Reference in New Issue
Block a user