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_idbundle_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/文件包)、存储后端等,落库后在列表可见该制品;③ 对线上制品行内补标签(如 prodrelease),用「部署历史」回看被哪些部署引用过;④ 配置清理策略(如 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
路由 nameApolloArtifacts
meta.titleApollo 制品管理
侧边栏入口ApolloLayout 侧边栏「制品管理」,位于「Git 仓库」之后、「流水线」之前
父路由/apollo(组件 ApolloLayoutmeta.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 认证与权限

2.3 端口与 API 前缀

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_pathplaceholder「如 /artifacts/pay-service/v1.2.0.tar.gz」非空去空格入 payload
大小(size_bytes)<input type="number" min="0">数字框;Number() 后仅 >0 入 payload空/0 不入 payload
SHA256artifactForm.sha256placeholder「64 位十六进制摘要」,等宽字体非空入 payload
Git 提交(git_commit_sha)artifactForm.git_commit_sha等宽字体非空入 payload
Git 分支(git_branch)artifactForm.git_branchplaceholder「如 main / release」非空入 payload
标签(tags)artifactForm.tagsText逗号分隔文本,placeholder「如 prod,release」split(',')+trim+过滤空后成数组,非空入 payload

提交按钮文案随忙碌态切换:busy === 'saveArtifact' 时显示「保存中...」并禁用,否则显示「注册」;另有一个 type="button" 的「取消」按钮直接关闭弹窗。点击「注册」(handleSaveArtifact):

  1. 先前端校验 name/version 非空,否则弹错误「名称(name)与版本(version)必填」并中止;
  2. busy='saveArtifact'POST /projects/1/artifacts(payload 见上表);
  3. 成功:从 data?.id || data?.artifact?.id 取新 id,提示 制品注册成功(id=N)alert-success),关闭弹窗并 fetchArtifacts() 刷新列表;
  4. 失败:提示「注册制品失败:<错误信息>」,弹窗不关闭、表单保留可修改重试;
  5. 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 | 标签 | 漏洞扫描 | 创建人 | 创建时间 | 操作。行渲染细节:

展示逻辑说明
IDa.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 位)
SHA256shortSha 取前 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_idenvLabel 翻译;状态 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 时后端直接返回「策略禁用不执行」的零统计结果(不删任何制品);统计字段是 CleanupResultscanned/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-Typeapplication/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.errorerror.message 供页面统一读取

5.2 端点表(路由注册见 server/server.goserver/artifact_handlers.go

方法路径权限点请求体 / Query说明
GET/projects/:id/artifactsartifact:read项目制品全量(id 升序),data 为数组
POST/projects/:id/artifactsartifact:writeRegisterRequest JSON注册制品;同名同版本 409;201 返回制品
GET/artifacts/:idartifact:read制品详情(页面未用,API 提供)
DELETE/artifacts/:idartifact:write删除制品;先按 StoragePath 删本地文件(bundle:// 前缀不删)再删元数据,返回 {deleted:id}
GET/artifacts/:id/deploymentsartifact:read制品部署引用列表(id 升序,无 status 字段)
GET/projects/:id/artifacts/searchartifact:readq关键字搜索:name LIKE '%q%'(q 空则全量)
GET/projects/:id/artifacts/versionsartifact:readname(必填)版本清单(页面未用;name 空返回 400)
POST/artifacts/:id/tagsartifact:write{tag}追加标签(幂等),返回更新后制品
DELETE/artifacts/:id/tags/:tagartifact:write移除标签(幂等),tag 空返回 400
POST/projects/:id/cleanup/runartifact:execute执行一次项目清理,返回 CleanupResult
GET/projects/:id/cleanup-policiesartifact:read项目清理策略列表(可空数组)
POST/projects/:id/cleanup-policiesartifact:write策略 JSON创建策略(201),(project_id, scope) 冲突 409
PUT/cleanup-policies/:idartifact:write策略 JSON(指针字段部分更新)更新策略,返回更新后策略
DELETE/cleanup-policies/:idartifact:write删除策略,返回 {deleted:id}
GET/projects/:id/environmentsenv: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_shaGit 提交注册可选
git_branchGit 分支注册可选
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,后端 ArtifactDeploymentid 升序;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_atsync_history_id 可空、非 GitOps 触发时记 0),被引用的制品在清理时强制保留,避免「正在线上使用的版本被回收」。需要如实说明的是:当前版本该方法只有单元测试调用,GitOps 包与 server handlers 均未接入(wiki 文档标注「部署引用 ◐ 未闭环」),因此 GET /artifacts/:id/deployments 在生产通常返回空——页面「部署历史」弹窗是为该能力预留的展示位,待 GitOps 联动落地后生效(见第 7 章问题 5/6)。

清理判定——一个完整的小例子。假设策略 scope=*keep_count=2keep_days=0keep_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. 权限与安全

7. 常见问题与排错

问题 1:注册制品时选择 S3 / 镜像仓库 / OSS 等存储后端被后端拒绝

问题 2:注册同名同版本制品被拒(ARTIFACT_CONFLICT)

问题 3:点「立即清理」后提示「清理完成:清理 0 个版本」且没删任何东西

问题 4:搜索时输入了版本号或 sha256 却搜不到

问题 5:部署历史提示「该制品暂无部署记录」

问题 6:制品确实被部署了,但部署历史仍为空

问题 7:删除制品/删除策略/立即清理点下去没反应或提示无权限

问题 8:部署历史弹窗的「环境」列显示成 #2 这类编号而不是环境名

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:00Z 落库的时间都被当作字面值展示(毫秒/偏移被截掉),跨时区或 UTC 落库时展示的是后端本地时刻而非浏览器本地时间
扫描状态只读展示vulnerability_scan_status 徽标仅展示字段,本页不触发扫描(扫描动作在安全合规/相关接口)
环境名依赖辅助接口环境下拉失败或记录环境已删除时,部署历史环境列退化为 #<id>
删除不可恢复删除制品连物理文件一并删除且无回收站;请先确认制品未被线上引用
确认框依赖原生 confirm删除/清理确认使用浏览器原生 window.confirm,样式不可定制且部分环境(iframe/禁用 confirm)不弹