1. 页面概览
1.1 是什么
「制品管理」页(对应前端源码 action/web/src/views/apollo/ArtifactsPage.vue,页面标题「Apollo 制品管理」)是 LightApollo 控制面的构建产物登记与版本治理中心,后端对齐 TAD-04「制品元数据管理」。制品(Artifact)在此采用「元数据注册」的自研轻量实现:页面并不上传文件本体,只登记 name / version / type / storage_backend / storage_path / size_bytes / sha256 / git_commit_sha / git_branch 等元数据到 artifacts 表,把「这条产物叫什么、是谁的哪个版本、放哪、多大、指纹多少」记录下来;在此基础上页面还提供标签体系(行内打标/删标)、部署历史回看(谁在哪次同步被部署到哪个环境)与清理策略(按 scope + 保留版本数/天数/标签批量淘汰历史版本)。
在整个 Apollo 产品的流程中,本页处于「流水线产出 → 制品沉淀 → 部署引用 → 过期回收」的制品生命环上:上游 CI/CD 流水线(pipelines 页)跑完一轮构建后,其产物以元数据形式登记为本页一条制品(后端字段 ci_pipeline_run_id、bundle_id 预留关联);下游部署若命中制品并写入 artifact_deployments 部署引用,就会把「该制品在某环境经某次 sync 被部署」记下来,从而让本页「部署历史」有据可查(注意:该写入的接线当前尚未在业务链路闭环,详见 5.4 与第 7 章问题 5/6);当制品版本不断累积,本页「立即清理」会按当前项目的清理策略(Cleanup Policy)把过期版本连同本地物理文件一并回收。输入是制品元数据与清理策略,输出是制品列表、可追溯的部署引用与回收统计。
需要特别区分的是 Apollo 里两套「产物」概念:本页管理的是制品仓库(轻量元数据,登记型、面向长期沉淀的制品库);而「制品与渠道」页(/apollo/bundles,BundlePage.vue)管理的 bundle 是可验签分发的最小单元(打包文件 + digest + SM2 签名,面向发布下载)。两者通过制品表的 bundle_id(指向 bundles.id)与 storage_path="bundle://{digest}" 连接键关联——bundle 被登记到制品仓库后,其本体引用统一以 bundle:// 表达,删除这类制品不会删 bundle 本体(详见第 5 章)。
典型使用链路(演示视角):① 先在某处产出产物(如流水线构建或手工打包),拿到底层文件的字节数/SHA256/Git 提交等元数据;② 本页点「注册制品」填名称(如 pay-service)、版本(如 v1.2.0)、类型(二进制/镜像/Helm/文件包)、存储后端等,落库后在列表可见该制品;③ 对线上制品行内补标签(如 prod、release),用「部署历史」回看被哪些部署引用过;④ 配置清理策略(如 scope=pay-service、保留最近 3 个或 30 天内),点「立即清理」批量回收过期版本;⑤ 若制品要作为可分发 bundle,则在「制品与渠道」页构建 bundle 后再回本页登记(或由后端自动登记),本页负责长期留存与引用保护。
1.2 核心价值/能力表
| 能力 | 说明 | 对应页面操作 |
|---|---|---|
| 元数据注册 | 不传文件只登记 name/version/type/存储信息等,快速沉淀产物清单,同名同版本冲突拦截(409) | 页头「注册制品」 |
| 制品全量列表 | 表格展示 ID/名称/版本/类型/大小/SHA256/标签/漏洞扫描状态/创建人/创建时间,类型与扫描状态彩色徽标 | 制品列表卡片(onMounted 拉取) |
| 关键字搜索 | 按关键字过滤制品(当前后端实现仅匹配制品名称,见 7 章排错) | 搜索框 +「搜索」/「清空」 |
| 标签维护 | 行内输入标签即点即打、标签 chip 旁 × 即删,删除接口对标签做 URL 编码 | 操作列「打标签」+ 标签「×」 |
| 部署历史追溯 | 查看制品被部署的环境/同步历史/时间,字段别名兼容 | 操作列「部署历史」 |
| 清理策略 | scope + keep_count/keep_days/keep_tags/enabled 五要素定义过期版本回收规则 | 顶部卡片「编辑策略」 |
| 立即清理 | 一键按当前策略执行回收,提示清理版本数与释放空间 | 顶部卡片「立即清理」 |
| 列表刷新联动 | 打标/删标/删除/注册/清理成功均自动重拉列表,保持视图与后端一致 | 各写操作成功回调 fetchArtifacts() |
1.3 一句话总结
制品管理页把「构建产物登记、标签维护、部署引用追溯、策略化清理」收敛为 Apollo 制品仓库的轻量入口——先登记元数据、再看被谁引用、最后按策略回收,是本产品所有可分发产物的「名册」所在。
2. 访问入口
2.1 路由与菜单
| 项 | 值 |
|---|---|
| 路由 path | /apollo/artifacts |
| 路由 name | ApolloArtifacts |
| meta.title | Apollo 制品管理 |
| 侧边栏入口 | ApolloLayout 侧边栏「制品管理」,位于「Git 仓库」之后、「流水线」之前 |
| 父路由 | /apollo(组件 ApolloLayout,meta.title: 'Apollo') |
| 前端源码 | action/web/src/views/apollo/ArtifactsPage.vue |
| API 客户端 | action/web/src/api/apolloClient.js |
| 路由注册 | action/web/src/router/index.js 第 433-438 行(children 内),requiresAuth: true,挂 ApolloLayout |
访问方式:登录 Apollo 后从左侧菜单「制品管理」进入,或直接访问 /apollo/artifacts。相邻页:上游是 git-repos.html(Git 仓库,制品来源之一),下游是 pipelines.html(CI/CD 流水线);分发侧见 bundles.html(制品与渠道,bundle 概念对照)。
2.2 认证与权限
- 路由
requiresAuth: true,无 guest 白名单:未登录访问会被前端路由守卫拦到/apollo/login。Apollo 控制面使用独立的apollo_token(localStorage),向后兼容无apollo_token时回退读取旧aip_token。 - 后端各制品端点全部注册在 protected 组并按权限点细粒度控制:读操作(列表/详情/部署历史/搜索/版本/策略查询)要求
PermArtifactRead(artifact:read);写操作(注册/删除/标签增删/策略增删改)要求PermArtifactWrite(artifact:write);清理执行(POST /projects/:id/cleanup/run)要求PermArtifactExecute(artifact:execute)。权限点缺失返回 HTTP 403。 - 403 由 apolloClient 响应拦截器统一
alert('无权限执行该操作');401 时拦截器清除apollo_token并跳转/apollo/login(登录页自身 401 不跳转,避免死循环)。
2.3 端口与 API 前缀
- Apollo 后端端口:18082;Vite 开发服务器把
/apollo-api前缀代理到该后端。 - 客户端
baseURL:/apollo-api/v1(apolloClient.js);统一 envelope{code, message, data, request_id}。 - 项目 id:页面暂用常量
PROJECT_ID = 1(default 项目),全部制品/策略端点落在/projects/1/...;/artifacts/:id/...全局资源端点不带项目段。 - 辅助接口:环境下拉走
GET /projects/1/environments,仅用于部署历史弹窗把environment_id翻译成环境名,失败不阻塞页面。
3. 界面布局
┌───────────────────────────────────────────────────────────┐
│ Apollo 制品管理 [注册制品] │
│ [alert-info 顶部说明:制品为"元数据注册"(TAD-04 自研轻量)] │
│ [操作结果提示条(v-if alert.message,右上角「关闭」)] │
├───────────────────────────────────────────────────────────┤
│ ┌ 清理策略(Cleanup Policy)卡片 ──────────────────────────┐ │
│ │ 标题 + [编辑策略] [立即清理] │ │
│ │ (加载中 / 空态:暂无清理策略,点击"编辑策略"创建 / 策略信息) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ [搜索框:按名称 / 版本 / sha256 / 标签搜索制品...][搜索][清空] │
├───────────────────────────────────────────────────────────┤
│ ┌ 制品列表卡片 ────────────────────────────────────────────┐ │
│ │ (加载中 / 空态 / 表格三态) │ │
│ │ ID 名称 版本 类型 大小 SHA256 标签 漏洞扫描 创建人 创建时间 操作 │ │
│ │ 操作列:标签输入+打标签 | 部署历史 | 删除 │ │
│ └─────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ ┌ 注册制品弹窗 modal ┐ ┌ 编辑清理策略弹窗 modal ┐ │
│ └───────────────────┘ └ 部署历史弹窗 modal(modal-lg)┘ │
└───────────────────────────────────────────────────────────┘
| 板块 | 职责 |
|---|---|
| 页头 | 标题「Apollo 制品管理」+ 右侧「注册制品」按钮(页头 flex 两端对齐) |
| 顶部说明条 | alert-info 常驻提示:制品为元数据注册、支持标签/部署历史/策略清理 |
| 操作结果提示条 | 全局唯一提示区(成功 alert-success/失败 alert-error/信息),右上角 link-btn「关闭」清空 |
| 清理策略卡片 | 展示当前项目第一条策略五要素;无策略显示创建引导空态;右上「编辑策略」「立即清理」 |
| 搜索行 | 关键字输入框(回车触发)+「搜索」+ 关键字非空时显示「清空」 |
| 制品列表卡片 | 加载中/空态/表格三态;行内打标签、删除标签、部署历史、删除 |
| 三个弹窗 | 注册制品表单(两列栅格)、编辑/新建清理策略表单、部署历史明细表(宽弹窗 modal-lg) |
4. 交互元素
4.1 「注册制品」按钮(页头)
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「注册制品」 | 页头右侧(page-header 内) | 打开「注册制品」弹窗,登记一条制品元数据 | 始终可用(不依赖选中行);点击后重置表单为初始值 | openCreateArtifact():artifactModal.visible=true,并把 artifactForm 重置为默认(type=binary、storage_backend=local、其余空串/空数组) | 无(纯前端) | 重置逻辑保证连续注册多条制品不会残留上一次输入;弹窗点遮罩(@click.self)同样关闭 |
4.2 注册制品弹窗与表单
弹窗标题为 注册制品(项目 #1),右上「关闭」;表单 @submit.prevent 提交,含必填「名称」「版本」等字段,全部字段如下:
| 字段(label) | 绑定/取值 | 必填 | 校验与默认 | 保存逻辑 |
|---|---|---|---|---|
| 名称(name) | artifactForm.name | 是(HTML required) | 前后去空格;为空则前端拦截「名称(name)与版本(version)必填」 | 去空格后入 payload |
| 版本(version) | artifactForm.version | 是(HTML required) | 同上 | 去空格后入 payload |
| 类型(type) | <select> 四选一 | 是(默认 binary) | 选项值 docker_image/binary/helm_chart/file_package,label 如「Docker 镜像(docker_image)」 | 直接入 payload |
| 存储后端(storage_backend) | <select> 五选一 | 否(默认 local) | 前端选项值 local/s3/minio/registry/oss(注意与后端白名单不一致,见第 7 章问题 1) | 空则兜底 local |
| 存储路径(storage_path) | artifactForm.storage_path | 否 | placeholder「如 /artifacts/pay-service/v1.2.0.tar.gz」 | 非空去空格入 payload |
| 大小(size_bytes) | <input type="number" min="0"> | 否 | 数字框;Number() 后仅 >0 入 payload | 空/0 不入 payload |
| SHA256 | artifactForm.sha256 | 否 | placeholder「64 位十六进制摘要」,等宽字体 | 非空入 payload |
| Git 提交(git_commit_sha) | artifactForm.git_commit_sha | 否 | 等宽字体 | 非空入 payload |
| Git 分支(git_branch) | artifactForm.git_branch | 否 | placeholder「如 main / release」 | 非空入 payload |
| 标签(tags) | artifactForm.tagsText | 否 | 逗号分隔文本,placeholder「如 prod,release」 | split(',')+trim+过滤空后成数组,非空入 payload |
提交按钮文案随忙碌态切换:busy === 'saveArtifact' 时显示「保存中...」并禁用,否则显示「注册」;另有一个 type="button" 的「取消」按钮直接关闭弹窗。点击「注册」(handleSaveArtifact):
- 先前端校验 name/version 非空,否则弹错误「名称(name)与版本(version)必填」并中止;
- 置
busy='saveArtifact',POST /projects/1/artifacts(payload 见上表); - 成功:从
data?.id || data?.artifact?.id取新 id,提示制品注册成功(id=N)(alert-success),关闭弹窗并fetchArtifacts()刷新列表; - 失败:提示「注册制品失败:<错误信息>」,弹窗不关闭、表单保留可修改重试;
finally清空 busy。
后端行为:同项目下(project_id, name, version)复合唯一,重复注册返回 HTTP 409 ARTIFACT_CONFLICT;类型/存储后端不在白名单返回 400;created_by 为空时后端用当前登录用户名填充。
4.3 搜索框与「搜索」「清空」按钮
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 搜索输入框 | 清理策略卡片之下、制品列表之上,独立搜索行 | 关键字过滤制品;placeholder「按名称 / 版本 / sha256 / 标签搜索制品...」 | 输入框回车(@keyup.enter)或点「搜索」触发 | handleSearch:关键字 trim 后置 searching=!!q,请求成功 toList(data) 覆写 artifacts,失败提示「搜索制品失败:...」并把列表置空 | GET /projects/1/artifacts/search?q=<关键字> | 「清空」仅在关键字非空时渲染 |
| 「搜索」 | 搜索行右侧 | 按关键字发起搜索 | 关键字可为空(空关键字等价查全量) | 调 handleSearch() | 同上 | 关键字 trim 后为空则不置 searching |
| 「清空」 | 搜索行右侧(v-if) | 退出搜索态、恢复全量 | 关键字非空才显示 | resetSearch():关键字置空、searching=false、重新 fetchArtifacts() 拉全量 | GET /projects/1/artifacts | 搜索态下列表为空显示「未搜索到匹配的制品。」,非搜索态空列表显示「暂无制品,点击"注册制品"添加(项目 #1)。」 |
后端搜索实现是 name LIKE '%kw%'(仅名称字段,第 5 章说明)——placeholder 声称的范围(名称/版本/sha256/标签)与真实能力不一致,见第 7 章问题 4。
4.4 制品列表表格与空态
列表表头:ID | 名称 | 版本 | 类型 | 大小 | SHA256 | 标签 | 漏洞扫描 | 创建人 | 创建时间 | 操作。行渲染细节:
| 列 | 展示逻辑 | 说明 |
|---|---|---|
| ID | a.id,等宽字体 | — |
| 名称 | a.name 加粗 | — |
| 版本 | a.version || '-',等宽 | 空版本显示 -(正常注册版本必填) |
| 类型 | typeClass(a.type) 彩色徽标 | docker_image 蓝 / binary 灰 / helm_chart 绿 / file_package 橙 / 未知紫色 type-other;空显示 unknown |
| 大小 | formatSize(size_bytes) | 空/非数字/负数显示 -;<1024 输出 N B;<1MB 输出 x.x KB;否则 x.x MB(10 位保留 1 位) |
| SHA256 | shortSha 取前 8 位,整值存 title 悬停提示 | 空显示 - |
| 标签 | tagList(a.tags) 兼容数组或逗号串;每个标签渲染 tag-chip(内含删除 ×) | 无标签显示灰色 - |
| 漏洞扫描 | scanClass(vulnerability_scan_status) 徽标 | clean/safe/passed/ok 绿;pending/scanning/queued/running 蓝;vulnerable/high/critical/failed/error 红;其余灰;值空显示 unknown。页面只展示字段,不触发扫描 |
| 创建人 | a.created_by || '-' | — |
| 创建时间 | formatTime(created_at):T→空格、去尾 Z、截前 19 位 | 输出 YYYY-MM-DD HH:mm:ss 样式 |
| 操作 | op-col 横向排布三组控件 | 见 4.5/4.7/4.8 |
toList 解析:data 为数组直接返回;否则取 data.items 数组;都不是返回空数组——适配后端 data 裸数组与分页 {items,...} 双形态。表格行以 a.id 为 key。列表加载失败(fetchArtifacts catch)会提示「加载制品列表失败:...」并把 artifacts 置空数组(见第 8 章)。
4.5 行内打标签(标签输入 + 「打标签」)
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 标签输入框 | 每行操作列第一组(tag-add) | 输入新标签内容 | placeholder「加标签」;输入为空点按钮 → 提示「请输入标签内容」 | 记录 tagInputs[a.id] | 无 | 输入框也可回车提交(@keyup.enter) |
| 「打标签」 | 每行操作列第一组(标签输入右侧) | 给该制品追加一个标签;后端幂等(已存在不重复) | 仅在非 busy 时可用 | handleAddTag(a):取 tagInputs[a.id] trim 后提交;成功提示 制品 #N 打标签 "xx" 成功,清空该行输入并 fetchArtifacts();失败提示「制品 #N 打标签失败:...」 | POST /artifacts/:id/tags(body {tag},PermArtifactWrite) | busy 键形如 addTag-<id>,全局互斥但行间各自区分 |
4.6 标签 chip 上的「×」(删除标签)
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 标签「×」 | 每个标签 chip 右侧小圆角 ×(title「删除标签」) | 移除该标签 | 始终可点(非 busy 时) | handleRemoveTag(a, tag):删除成功提示 已删除制品 #N 的标签 "xx" 并 fetchArtifacts() | DELETE /artifacts/:id/tags/:tag,标签经 encodeURIComponent 编码(兼容含 /、#、空格等特殊字符的标签) | 无二次确认,点击即删(后端幂等,删不存在的标签不报错);busy 键 delTag-<id>-<tag> |
4.7 「部署历史」按钮与弹窗
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「部署历史」 | 每行操作列第二组 | 弹窗查看该制品的历史部署引用(artifact_deployments 记录) | 始终可点 | openDeployments(a):弹窗标题 部署历史:{name}(#id),先置 loading,请求成功解析数据,空则显示「该制品暂无部署记录。」 | GET /artifacts/:id/deployments(PermArtifactRead) | 响应可能是数组或 {deployments|items:[...]} 嵌套,统一 Array.isArray ? data : toList(data?.deployments || data) 解析 |
弹窗内表格列:ID | 部署时间 | 同步历史 ID | 环境 | 状态:部署时间取 deployed_at 优先、created_at 兜底(formatTime);同步历史 ID 在 sync_history_id / sync_id / history_id 间取首个非空,空显示 -;环境用 environment_id / environment / env_id 经 envLabel 翻译;状态 d.status || 'success' 并套 deployStatusClass 徽标。后端 artifact_deployments 表本身无 status 字段,故状态列实际恒显示绿色「success」(见第 8 章缺陷);环境名依赖 GET /projects/1/environments 下拉,找不到该 id 时退化为 #<id>。弹窗遮罩点击与「关闭」均可退出。
4.8 「删除」制品按钮
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「删除」 | 每行操作列第三组(btn-danger) | 删除该制品(元数据 + 本地物理文件) | 始终可点;操作前必弹 window.confirm | 确认框文案 确定删除制品 "{name}"(#{id},{version||未知版本})吗?;确认后删除,成功提示「制品已删除」并 fetchArtifacts();失败提示「删除制品失败:...」 | DELETE /artifacts/:id(PermArtifactWrite) | 后端删除前若 StoragePath 非空且不以 bundle:// 开头,会先删 data/artifacts/<StoragePath> 物理文件并 best-effort 清理空版本目录,再删元数据行(bundle:// 关联制品只删元数据、不删 bundle 本体);已启用清理策略的保留语义与「立即清理」冲突时以本删除为准(直接强制删除) |
4.9 清理策略卡片与「编辑策略」按钮
顶部卡片标题 清理策略(Cleanup Policy),右上「编辑策略」「立即清理」两按钮(均 :disabled="!!busy")。卡片主体三态:
| 状态 | 展示 | 说明 |
|---|---|---|
| 加载中 | policyLoading 时显示灰色「加载中...」 | 仅挂载时短暂出现 |
| 无策略 | 暂无清理策略,点击"编辑策略"创建(项目 #1)。 | 后端返回空数组(或 404)时 policy=null,属正常初始态 |
| 有策略 | policy-grid 五格展示 | 每格 label+value,见下表 |
策略信息格(label 均为后端字段名 + 中文):作用范围(scope)/ 保留版本数(keep_count)/ 保留天数(keep_days)/ 保留标签(keep_tags)/ 启用(enabled)。keep_count ?? '-'、keep_days ?? '-' 用空值合并;keep_tags 数组 join , ,字符串原样,空显示 -;enabled 徽标:false 显示 disabled(灰 status-offline),否则 enabled(绿 status-online)。
「编辑策略」→ openEditPolicy():以当前 policy(无则空对象)回填表单并打开策略弹窗。表单初始值来自策略字段;enabled 勾选默认 p.enabled !== false(即只要不是显式 false 都视为启用)。
4.10 清理策略弹窗(新建 / 编辑 / 删除)
弹窗标题:有策略时 编辑清理策略 #{id}(项目 #1),无策略时 新建清理策略(项目 #1)。字段如下:
| 字段 | 类型/绑定 | 必填 | 默认与校验 | 保存语义 |
|---|---|---|---|---|
| 作用范围(scope) | policyForm.scope,文本框 | 是 | placeholder「如 * 或 pay-service」;空拦截「作用范围(scope)必填」;副文案「制品名称匹配模式(* 表示全部)。」 | trim 后入 payload |
| 保留版本数(keep_count) | type="number" min="0" | 否 | 空/0 不携带;Number()>=0 才入 | 0 表示不按数量维度限制 |
| 保留天数(keep_days) | type="number" min="0" | 否 | 同上 | 0 表示不按天数限制 |
| 保留标签(keep_tags) | policyForm.keep_tagsText,逗号分隔 | 否 | placeholder「如 prod,release(命中则不被清理)」 | 切分 trim 后成数组,非空携带 |
| 启用该策略 | <input type="checkbox"> 行内 | 否 | 默认勾选 | !!enabled 始终携带 |
底部按钮:有策略时出现红色「删除策略」(handleDeletePolicy,先 window.confirm('确定删除清理策略 #{id} 吗?') 再 DELETE /cleanup-policies/:id,成功提示「清理策略已删除」并关闭弹窗刷新);另有「取消」「保存」(保存中显示「保存中...」)。
「保存」(handleSavePolicy)分支:policy.value?.id 存在则 PUT /cleanup-policies/:id 并提示 清理策略 #N 更新成功;否则 POST /projects/1/cleanup-policies,从 data?.id || data?.policy?.id 取新 id 提示 清理策略创建成功(id=N)。失败统一提示「保存清理策略失败:...」。成功后关闭弹窗并 fetchPolicy() 刷新卡片。注意「更新」与「创建」的判定依据是卡片当前策略,不是弹窗中 id。
4.11 「立即清理」按钮
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「立即清理」 | 清理策略卡片右上(btn-primary) | 立即按当前项目清理策略执行一次回收(执行权限点 PermArtifactExecute) | 非 busy 时可用;点击必弹 window.confirm | 确认框文案 确定立即执行清理吗?将按当前策略删除过期制品版本。;确认后执行,成功后从响应按别名取 removed_count/deleted_count/count/removed/total(缺省 0)与 freed_bytes/reclaimed_bytes,拼提示 清理完成:清理 N 个版本,释放 X(可选附带后端 message/summary);removed>0 显示 alert-success,否则 alert-info;随后 fetchArtifacts() 刷新 | POST /projects/1/cleanup/run(PermArtifactExecute) | 后端在执行前会幂等补齐默认策略(见 5.4),故首次「立即清理」即使从未配置策略也会先落一条 keep_count=10 的默认策略再执行;策略 enabled=false 时后端直接返回「策略禁用不执行」的零统计结果(不删任何制品);统计字段是 CleanupResult 的 scanned/deleted_artifacts/freed_bytes/...(见 5.3),前端用别名映射取值 |
5. 后端关联
5.1 API 客户端
| 项目 | 值 |
|---|---|
| 客户端文件 | action/web/src/api/apolloClient.js |
| baseURL | /apollo-api/v1(Vite 代理到 Apollo 后端 18082) |
| 超时 | 30000 ms |
| 默认 Content-Type | application/json |
| 请求拦截器 | 从 getApolloToken()(apollo_token 优先、回退 aip_token)取 token,附加 Authorization: Bearer <token> |
| 响应拦截器(成功) | HTTP 2xx 且 body.code 为数字 0 → 把 response.data 替换为业务数据(页面 const { data } 直取);blob/arraybuffer 原样放行;无数字 code 的裸响应原样返回 |
| 响应拦截器(失败) | 401 → 清 apollo_token 并跳 /apollo/login(登录页自身不跳);403 → alert('无权限执行该操作');服务端 message 会写入 error.response.data.error 与 error.message 供页面统一读取 |
5.2 端点表(路由注册见 server/server.go 与 server/artifact_handlers.go)
| 方法 | 路径 | 权限点 | 请求体 / Query | 说明 |
|---|---|---|---|---|
| GET | /projects/:id/artifacts | artifact:read | — | 项目制品全量(id 升序),data 为数组 |
| POST | /projects/:id/artifacts | artifact:write | RegisterRequest JSON | 注册制品;同名同版本 409;201 返回制品 |
| GET | /artifacts/:id | artifact:read | — | 制品详情(页面未用,API 提供) |
| DELETE | /artifacts/:id | artifact:write | — | 删除制品;先按 StoragePath 删本地文件(bundle:// 前缀不删)再删元数据,返回 {deleted:id} |
| GET | /artifacts/:id/deployments | artifact:read | — | 制品部署引用列表(id 升序,无 status 字段) |
| GET | /projects/:id/artifacts/search | artifact:read | q | 关键字搜索:name LIKE '%q%'(q 空则全量) |
| GET | /projects/:id/artifacts/versions | artifact:read | name(必填) | 版本清单(页面未用;name 空返回 400) |
| POST | /artifacts/:id/tags | artifact:write | {tag} | 追加标签(幂等),返回更新后制品 |
| DELETE | /artifacts/:id/tags/:tag | artifact:write | — | 移除标签(幂等),tag 空返回 400 |
| POST | /projects/:id/cleanup/run | artifact:execute | — | 执行一次项目清理,返回 CleanupResult |
| GET | /projects/:id/cleanup-policies | artifact:read | — | 项目清理策略列表(可空数组) |
| POST | /projects/:id/cleanup-policies | artifact:write | 策略 JSON | 创建策略(201),(project_id, scope) 冲突 409 |
| PUT | /cleanup-policies/:id | artifact:write | 策略 JSON(指针字段部分更新) | 更新策略,返回更新后策略 |
| DELETE | /cleanup-policies/:id | artifact:write | — | 删除策略,返回 {deleted:id} |
| GET | /projects/:id/environments | env:read | — | 环境列表(部署历史弹窗翻译环境名) |
5.3 响应结构示例
制品列表响应(GET /projects/1/artifacts,经 apolloClient 解包后页面拿到的即为 data 数组):
{
"code": 0,
"message": "ok",
"data": [
{
"id": 1,
"project_id": 1,
"name": "pay-service",
"version": "v1.2.0",
"type": "binary",
"storage_backend": "local",
"storage_path": "1/pay-service/v1.2.0/pay-service",
"size_bytes": 10485760,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"tags": ["prod", "release"],
"git_commit_sha": "a1b2c3d4",
"git_branch": "main",
"ci_pipeline_run_id": 0,
"bundle_id": 0,
"vulnerability_scan_status": "unscanned",
"created_by": "admin",
"created_at": "2026-08-30T10:00:00+08:00",
"updated_at": "2026-08-30T10:00:00+08:00"
}
],
"request_id": "req_1724992800000ab12"
}
制品记录字段字典(Artifact 模型,artifacts 表):
| 字段(JSON) | 含义 | 页面来源/说明 |
|---|---|---|
id | 制品自增主键 | 列表首列 |
project_id | 归属项目((project_id,name,version) 复合唯一) | 前端硬编码 1 |
name | 制品名称(如 pay-service) | 注册必填 |
version | 制品版本(如 v1.2.0) | 注册必填 |
type | 类型:docker_image/binary/helm_chart/file_package | 注册下拉;空则后端兜底 file_package |
storage_backend | 存储后端:local/harbor/minio/aliyun_oss | 注册下拉;空兜底 local |
storage_path | 相对 data/artifacts 的路径;无文件为空;bundle://{digest} 关联 bundle 本体 | 注册可选;bundle:// 前缀保护文件 |
size_bytes | 字节数(0 表示未登记) | 注册可选,formatSize 展示 |
sha256 | 文件 SHA256(64 位十六进制) | 注册可选,列表仅显前 8 位 |
tags | 标签 JSON 数组 | 注册逗号串/行内打标 |
metadata | 扩展元数据 JSON | 页面未暴露,API 支持 |
git_commit_sha | Git 提交 | 注册可选 |
git_branch | Git 分支 | 注册可选 |
ci_pipeline_run_id | 关联流水线运行(预留) | 页面不展示 |
bundle_id | 关联 bundles.id(预留) | 页面不展示;被 bundle 清理引用保护 |
vulnerability_scan_status | 漏洞扫描状态:unscanned/scanning/clean/vulnerable | 列表「漏洞扫描」列;本页只读不触发 |
created_by | 创建人(空时后端填当前用户名) | 列表「创建人」列 |
created_at | 创建时间(RFC3339) | 列表「创建时间」列 |
updated_at | 更新时间 | 页面不展示 |
时间展示说明:时间字段是 RFC3339 带时区格式。页面 formatTime 仅做三次字符处理——T→空格、去掉 Z 字符、slice(0,19) 截前 19 位——不做任何时区换算。效果是:无论后端落的是 UTC(Z 结尾)还是本地偏移(如 +08:00,后端 JSON 常带毫秒如 2026-09-06T02:47:07.8813266+08:00),页面都只显示其日期+时刻的「字面值」,时区与毫秒一律截掉。演示若全部由本机后端生成(本地时区写入)则观感正常;一旦接入跨时区或 UTC 落库的数据源,展示的就是「后端本地时刻」而非浏览器本地时间(见第 8 章「时间展示无时区换算」)。
清理执行响应(POST /projects/1/cleanup/run,后端 CleanupResult):
{
"code": 0,
"message": "ok",
"data": {
"scanned": 12,
"deleted_artifacts": 3,
"freed_bytes": 5242880,
"skipped_referenced": 1,
"skipped_tagged": 2,
"skipped_recent": 4,
"policy_used": "project",
"policy_enabled": true
},
"request_id": "req_1724992800000cd34"
}
字段含义:scanned 扫描的制品总数;deleted_artifacts 实际删除数(前端以 removed_count/deleted_count/... 别名读取,核心即此值);freed_bytes 释放字节数;skipped_referenced 被部署引用强制保留数;skipped_tagged 命中 keep_tags 保留数;skipped_recent 命中 keep_days 保留数;policy_used 使用的作用域(project);policy_enabled 策略是否启用。
清理策略对象(GET/POST/PUT 均返回同构):
{
"code": 0,
"message": "ok",
"data": {
"id": 1,
"project_id": 1,
"scope": "*",
"keep_count": 3,
"keep_days": 30,
"keep_tags": ["prod"],
"enabled": true,
"created_at": "2026-08-30T10:00:00+08:00",
"updated_at": "2026-08-30T10:00:00+08:00"
},
"request_id": "req_1724992800000ef56"
}
制品部署历史响应(GET /artifacts/:id/deployments,后端 ArtifactDeployment,id 升序;sync_history_id 可空、非 GitOps 触发部署为 0,deployed_at 为空时页面回退 created_at):
{
"code": 0,
"message": "ok",
"data": [
{
"id": 1,
"artifact_id": 2,
"sync_history_id": 11,
"environment_id": 2,
"deployed_at": "2026-08-30T11:00:00+08:00",
"created_at": "2026-08-30T11:00:00+08:00"
},
{
"id": 2,
"artifact_id": 2,
"sync_history_id": 0,
"environment_id": 1,
"created_at": "2026-09-01T09:30:00+08:00"
}
],
"request_id": "req_1724992800000a0f1"
}
注意(重要):当前版本 artifact_deployments 通常没有数据——写入它的 RegistryService.RecordDeployment 只有单元测试调用,GitOps 引擎与 HTTP handlers 尚未接线(wiki 标注「部署引用 ◐ 未闭环」)。页面「部署历史」弹窗与上方示例是预留展示位;演示时该接口大概率返回空数组,前端显示「该制品暂无部署记录。」,不代表页面故障(见第 7 章问题 5/6)。
注册请求 payload(buildArtifactPayload 产物,POST /projects/1/artifacts,供比对后端 RegisterRequest 字段):
{
"name": "pay-service",
"version": "v1.2.0",
"type": "binary",
"storage_backend": "local",
"storage_path": "/artifacts/pay-service/v1.2.0.tar.gz",
"size_bytes": 10485760,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"git_commit_sha": "a1b2c3d4",
"git_branch": "main",
"tags": ["prod", "release"]
}
注意:storage_path 在请求里是手工填写的描述性路径;若请求带 file_data(base64)后端才会真正落盘并回写计算出的相对路径/SHA256/大小。type/storage_backend 均必填合法值(后端空 type 兜底 file_package、空 backend 兜底 local);metadata/ci_pipeline_run_id/bundle_id 等扩展字段页面表单未暴露,仅 API/服务层支持。
5.4 关键机制
元数据注册与唯一约束。artifacts 表以 (project_id, name, version) 复合唯一索引兜底,同一项目下同名同版本只能登记一次;重复注册在服务层与数据库两层都拦为 HTTP 409 ARTIFACT_CONFLICT(错误文案「制品 X:Y 已存在」)。服务层同时校验:名称/版本非空(400)、类型在白名单(docker_image/binary/helm_chart/file_package)、存储后端在白名单(后端合法值为 local/harbor/minio/aliyun_oss)。vulnerability_scan_status 注册时恒为 unscanned,后续由安全合规相关接口(POST /artifacts/:id/scan、/sbom 等,PermSecurityExecute)更新——本页只读展示该字段。注册接口实际也支持 file_data(base64 字节)把文件本体写入 data/artifacts/{project}/{name}/{version}/{file} 并自动算 SHA256/大小/路径,但前端表单不上传文件,仅以元数据方式登记;若带文件,后端会对 name/version/fileName 做路径段安全校验(拒绝空段、.、..,防路径穿越),并把分隔符替换为 _。
搜索语义。/projects/:id/artifacts/search 的后端实现仅 name LIKE '%<q>%' 一条过滤(q 为空返回项目全量),不匹配 version/sha256/tags——与前端 placeholder「按名称 / 版本 / sha256 / 标签搜索制品...」所暗示的范围不一致(见第 7 章问题 4 与第 8 章缺陷)。
清理策略语义与 Cleanup 执行链。项目清理策略以 (project_id, scope='project') 为键、每项目至多一条有效策略(本页只展示 list[0])。执行清理(Cleanup)时:① 先幂等补齐默认策略(EnsureDefaultPolicy:不存在则建 keep_count=10、keep_days=0、keep_tags=[]、enabled=true)——这就是「从未配置也能立即清理」的原因;② 若策略 enabled=false 直接返回零统计,不删任何制品;③ 把制品按 name 分组、组内按 CreatedAt 降序(ID 降序破平),逐组圈定四类强制保留:命中 keep_tags(任一标签交集)、keep_days 保留期内新建、被 artifact_deployments 引用(referencedArtifactIDs Distinct 查询)、每 name 最近 keep_count 个;④ 其余候选先删物理文件(StoragePath 空或 bundle:// 前缀不删本地文件,removeFile best-effort 清理空目录)再删元数据行;⑤ 统计返回 CleanupResult。清理为项目级一次性同步执行(无后台任务、无调度),页面刷新列表即可看到被清理的版本消失。
部署引用与 bundle 关联。artifact_deployments 表由后端服务方法 RegistryService.RecordDeployment(artifactID, syncHistoryID, environmentID) 写入一行(artifact_id + sync_history_id + environment_id + deployed_at,sync_history_id 可空、非 GitOps 触发时记 0),被引用的制品在清理时强制保留,避免「正在线上使用的版本被回收」。需要如实说明的是:当前版本该方法只有单元测试调用,GitOps 包与 server handlers 均未接入(wiki 文档标注「部署引用 ◐ 未闭环」),因此 GET /artifacts/:id/deployments 在生产通常返回空——页面「部署历史」弹窗是为该能力预留的展示位,待 GitOps 联动落地后生效(见第 7 章问题 5/6)。
清理判定——一个完整的小例子。假设策略 scope=*、keep_count=2、keep_days=0、keep_tags=["prod"]、enabled=true,同项目制品表里 pay-service 名下先后有 v1.0.0、v1.1.0、v1.2.0、v1.3.0 四行(创建时间递增,即 v1.3.0 最新),其中 v1.2.0 打了 prod 标签、v1.3.0 被一条 artifact_deployments 引用。执行「立即清理」时后端按 name 分组、组内按 created_at 降序(ID 降序破平)排列后单遍打标:v1.3.0 同时命中「keep_count 前 2」「被引用」两个条件、v1.2.0 同时命中「keep_count 前 2」「keep_tags=prod」,v1.1.0/v1.0.0 位于第 3、4 位且无其它保留理由 → 判为过期,先删物理文件再删元数据行。结果提示「清理完成:清理 2 个版本,释放 X」,v1.2.0/v1.3.0 留在列表。注意 keep_count 是「组内最近 N 个全保」而不是「扣掉强制保留后补 N 个」——四种保留条件是并集关系。若把 keep_days 改为 30 而 v1.1.0/v1.0.0 都在 30 天内新建,则它们又命中 keep_days 保留,结果变成「清理 0 个版本」——这正是问题 3 里「点清理却什么都没删」的常见成因。制品与 bundle 的关联走两条通道:bundle_id 列直接指向 bundles.id,或 storage_path="bundle://{digest}" 表达 bundle 本体连接键——删除制品时 handler 见 bundle:// 前缀即跳过物理文件删除,保证不会误删 bundle 分发本体(bundle 有自己的生命周期清理,见「制品与渠道」页)。
部署历史字段形态。ListDeployments 直接查 artifact_deployments 行,逐行 JSON 仅含 id/artifact_id/sync_history_id/environment_id/deployed_at/created_at,没有 status;前端状态列 d.status || 'success' 因而恒显示成功。环境名列无法从行数据得知名字,前端用预取的 /projects/1/environments 列表做 id→name 翻译。
audit 审计。制品关键写操作均在 handler 层记审计:注册 ARTIFACT_CREATE、删除 ARTIFACT_DELETE、执行清理 ARTIFACT_CLEANUP_RUN(含 project_id/name/version/deleted/freed_bytes 明细),可在「审计日志」页追溯。
6. 权限与安全
- 认证分层:全部制品端点挂 protected 组,JWT/登录态失效返回 401,前端响应拦截器清 token 跳
/apollo/login;无apollo_token时回退aip_token属向后兼容设计,新环境建议统一用 Apollo 独立登录。 - 权限点隔离:读(
artifact:read)/写(artifact:write)/执行(artifact:execute)三权分离——「立即清理」是唯一需要 execute 权限点的操作,普通可读可写角色若缺 execute 会在点击时被 403 拦截。 - 写操作防护:删除制品、删除策略、立即清理均走
window.confirm二次确认;busy 全局互斥防重复提交(忙碌中所有行按钮置灰)。 - 路径与数据安全:后端对制品 name/version/文件名的路径段做消毒与穿越校验(
sanitizeSegment+validateSegment),防止写文件越出data/artifacts根目录;删除策略/清理对物理文件的删除均有目录归属校验,bundle://引用不连带删除 bundle 本体。制品元数据中可能含 sha256/Git 提交等供应链信息,对外导出/截图分享时按需脱敏。 - 审计留痕:注册/删除/清理均有 AUDIT 记录(带操作者、对象、明细),可在审计日志页回溯谁在何时删改了制品或执行了清理。
7. 常见问题与排错
问题 1:注册制品时选择 S3 / 镜像仓库 / OSS 等存储后端被后端拒绝
- 现象:选了「S3」或「镜像仓库(registry)」或「OSS」点「注册」,提示「注册制品失败:非法存储后端 "s3"」。
- 原因:前端下拉的选项值
s3 / registry / oss与后端存储后端白名单local / harbor / minio / aliyun_oss不一致(前端STORAGE_BACKENDS选项名与后端validStorageBackends枚举对不上),只有local、minio两者能通过后端校验。 - 处理:当前需先选「本地存储(local)」或「MinIO」完成登记(storage_path 可手工填远端路径表达意图);该不一致已在缺陷报告中登记,等待前后端统一枚举(第 8 章)。
问题 2:注册同名同版本制品被拒(ARTIFACT_CONFLICT)
- 现象:点「注册」提示「注册制品失败:项目 1 下制品 pay-service:v1.2.0 已存在」或「制品 pay-service:v1.2.0 已存在」。
- 原因:
(project_id, name, version)复合唯一,重复登记由服务层与数据库唯一索引双重拦截为 HTTP 409。 - 处理:换一个新版本号(如
v1.2.1)登记;若确需覆盖旧定义,先「删除」旧制品再登记(无版本升级语义)。
问题 3:点「立即清理」后提示「清理完成:清理 0 个版本」且没删任何东西
- 现象:确认执行后结果提示清理 0 个版本、释放 0。
- 原因:策略被停用(
enabled=false,后端直接跳过执行);或当前制品都在保留范围内(命中 keep_tags/keep_days/被部署引用/每名最近 keep_count 个)。 - 处理:去「编辑策略」确认「启用该策略」勾选状态与各阈值;若只是想验证,把 keep_count 调小/keep_days 置 0 后重跑,或查看后端审计
ARTIFACT_CLEANUP_RUN的 deleted 明细。
问题 4:搜索时输入了版本号或 sha256 却搜不到
- 现象:按 placeholder 提示输入版本(如
v1.2.0)或 sha256 关键字点「搜索」,列表提示「未搜索到匹配的制品。」。 - 原因:后端搜索实现是
name LIKE '%q%',只匹配名称字段;输入非名称关键字自然无结果(符合后端契约,但与界面提示的范围不符)。 - 处理:改用制品名称关键字搜索;需要按版本/sha256/标签检索时,暂用「清空」后人工在列表定位,或后续等后端搜索能力扩展。
问题 5:部署历史提示「该制品暂无部署记录」
- 现象:点「部署历史」弹窗显示空态。
- 原因:该制品从未被任何部署引用写入
artifact_deployments(见问题 6——当前版本后端RecordDeployment尚无业务调用方,部署记录通常就是空的),或部署记录表中确实无artifact_id匹配行。 - 处理:先确认该制品确实被部署过(部署与漂移页查看对应 sync 历史);制品表与部署记录都只删不更,若记录确实存在但弹窗为空,检查浏览器 Network 中
/artifacts/:id/deployments的响应是否为数组/{deployments}形态。
问题 6:制品确实被部署了,但部署历史仍为空
- 现象:在部署与漂移页对某制品对应的环境执行过 sync,回本页看「部署历史」仍是空态。
- 原因:写入
artifact_deployments的RegistryService.RecordDeployment目前只有单元测试调用,GitOps 引擎与 HTTP handlers 都还没有接线(wiki 标为「◐ 未闭环」);无论同步成功与否,当前版本都不会自动产生制品部署引用行。 - 处理:这不是页面操作错误。可先用后端
deploy-history/sync-history类命令或部署与漂移页确认同步本身成功;制品部署历史能力需待 GitOps 联动落地后才有数据(该页弹窗为预留展示位)。
问题 7:删除制品/删除策略/立即清理点下去没反应或提示无权限
- 现象:点击后无对话框,或弹出「无权限执行该操作」。
- 原因:浏览器拦截了原生
window.confirm(如被 iframe/设置禁用)时点击无反应;403 则是当前账号缺少artifact:write(删除)或artifact:execute(清理)权限点。 - 处理:确认账号在角色管理中被授予对应权限点;原生 confirm 被禁时改用直连 API(携带 apollo_token)验证,页面功能本身依赖 confirm 确认。
问题 8:部署历史弹窗的「环境」列显示成 #2 这类编号而不是环境名
- 现象:部署历史有数据时,环境列只显示
#<数字>,看不到环境名称。 - 原因:
envLabel用environment_id去GET /projects/1/environments预取的列表里查找;后端没配任何环境、环境下拉失败(fetchEnvironments静默降级为空数组)、或部署记录引用的环境已被删除时,找不到匹配项就退化显示#<id>(找到则显示「名称(#id)」)。 - 处理:先到环境管理页确认存在至少一个环境且与部署记录里的
environment_id对应;刷新本页重新加载;若环境确被删除,该历史行的环境名无法还原,属正常兜底。
8. 已知缺陷与边界
| 缺陷/边界 | 说明 |
|---|---|
| 项目 id 硬编码 | PROJECT_ID = 1 写死为 default 项目,多项目场景需改为从 URL /project_id 读取(源码注释已标注待办) |
| 存储后端枚举前后端不一致 | 前端下拉选项 local/s3/minio/registry/oss,后端白名单 local/harbor/minio/aliyun_oss;选 s3/registry/oss 会被后端 400 拒绝 |
| 搜索范围与提示不符 | placeholder 声称按「名称/版本/sha256/标签」搜索,后端仅 name LIKE;版本/sha 等检索能力缺失 |
| 清理策略仅取第一条 | 页面 policy = list[0],同项目多条策略(如多 scope)不覆盖 |
| 部署历史通常为空 | RecordDeployment 只有单测调用、GitOps/handlers 未接线,artifact_deployments 无生产数据,部署历史弹窗多为空态(预留能力) |
| 部署历史状态列失真 | artifact_deployments 无 status 字段,前端 d.status || 'success' 使状态列恒为成功,误导性展示 |
| 搜索失败清空列表 | handleSearch 的 catch 把 artifacts 置空数组(连同已加载的全量数据一起丢失),无任何残留提示以外的可见状态 |
| 时间展示无时区换算 | formatTime 仅做 T→空格、去 Z、截 19 位,不做时区换算:+08:00 与 Z 落库的时间都被当作字面值展示(毫秒/偏移被截掉),跨时区或 UTC 落库时展示的是后端本地时刻而非浏览器本地时间 |
| 扫描状态只读展示 | vulnerability_scan_status 徽标仅展示字段,本页不触发扫描(扫描动作在安全合规/相关接口) |
| 环境名依赖辅助接口 | 环境下拉失败或记录环境已删除时,部署历史环境列退化为 #<id> |
| 删除不可恢复 | 删除制品连物理文件一并删除且无回收站;请先确认制品未被线上引用 |
| 确认框依赖原生 confirm | 删除/清理确认使用浏览器原生 window.confirm,样式不可定制且部分环境(iframe/禁用 confirm)不弹 |