skillhub/docs/05-business-flows.md
dongmucat 256344cbde fix(review): allow super admin self promotion review
Signed-off-by: dongmucat <1127093059@qq.com>
2026-06-16 11:29:39 +08:00

14 KiB
Raw Permalink Blame History

skillhub 核心业务流

1 发布流程

一期采用同步发布模型:上传、校验、存储、持久化在一次请求中同步完成。前端通过异步上传(带进度条)提升用户体验,但后端处理是同步的。

设计决策一期暂不考虑异步发布uploadId、publishId、状态轮询、异步转正等。一期技能包为文本资源包体积有限上限 10MB同步处理足以满足需求。如后续引入大文件或复杂校验流程再考虑异步模型。

1.1 当前发布流程基线

用户提交发布
    │
    ▼
① 身份与权限校验(用户是否为该 namespace 的 MEMBER 以上)
    │
    ▼
② 技能包校验
   - SKILL.md 存在性、frontmatter 格式
   - 文件类型白名单、单文件大小限制、总包大小限制
   - 版本号 semver 合法性、不与已有版本冲突
   - [扩展点] PrePublishValidator 链(一期空实现)
    │
    ▼
③ 同步写入对象存储
   - 文件逐个上传到正式路径 `skills/{skillId}/{versionId}/{filePath}`,记录 SHA-256
   - 生成预打包 zip 到 `packages/{skillId}/{versionId}/bundle.zip`
    │
    ▼
④ 持久化数据
   - 创建或关联 skill 记录(首次发布时创建 skill
   - 创建 skill_version普通用户进入 `PENDING_REVIEW``SUPER_ADMIN` 直达 `PUBLISHED`
   - 创建 skill_file 记录
   - 解析 SKILL.md frontmatter → parsed_metadata_json
   - 生成 manifest_json
   - 直发场景更新 skill.latest_version_id
    │
    ▼
⑤ 同步写入审计日志
    │
    ▼
⑥ 异步触发搜索索引写入

当前版本采用审核流不再区分“Phase 2 直发”与“Phase 3 恢复审核”两套现实实现:

  • 普通用户发布请求创建 skill_version(status=PENDING_REVIEW)
  • 同步创建 review_task(status=PENDING)
  • 审核通过后转为 PUBLISHED
  • 审核拒绝后转为 REJECTED
  • 撤回审核时删除 PENDING review_task,并将 skill_version 回退到 DRAFT
  • 例外:提交人持有 SUPER_ADMIN 平台角色时,发布入口直接创建 skill_version(status=PUBLISHED),跳过 review_task 创建,同时不再要求其必须是目标 namespace 成员
  • 上述例外必须对 Web、/api/v1/publish/api/v1/publish 保持一致
  • 若重传新版本时发现旧的 PENDING_REVIEW 版本,旧版本会被自动降回 DRAFT,再创建新的待审版本

1.2 生命周期读模型

当前代码中的 skill 生命周期展示与操作判断,不再依赖旧的 latestVersionStatusviewingVersionStatus 一类拼装字段,而统一基于以下 projection

  • headlineVersion:当前详情页/我的技能列表主展示版本
  • publishedVersion:当前最新可公开分发的已发布版本
  • ownerPreviewVersion:详情 projection 中仅暴露给 owner / namespace 管理者的 PENDING_REVIEW 版本
  • resolutionModePUBLISHED / OWNER_PREVIEW / NONE

业务规则:

  • 公开入口只认 publishedVersion
  • owner 进入详情页时,如果没有可用 publishedVersion,才允许 headlineVersion = ownerPreviewVersion
  • 推广到全局、安装命令、公开下载都只能绑定到 publishedVersion
  • hidden 是独立治理覆盖层,不属于 skill 生命周期状态机

1.3 Skill 可见性与角色访问矩阵

以下矩阵以当前后端实现为准,综合了 VisibilityCheckerSkillQueryServiceSkillDownloadServiceReviewPermissionChecker 的实际行为。

1.3.1 Skill 容器读取

角色 PUBLIC NAMESPACE_ONLY PRIVATE hidden 任意 visibility publishedVersionlatest_version_id=null
匿名用户 可读 不可读 不可读 不可读 不可读
登录非成员 可读 不可读 不可读 不可读 不可读
namespace MEMBER 可读 可读 不可读 不可读 仅自己是 owner 时可读
skill owner 可读 可读 可读 可读 可读
namespace ADMIN / OWNER 可读 可读 可读 可读 不可读,除非本人也是 skill owner
SKILL_ADMIN / SUPER_ADMIN仅平台角色 与普通登录用户一致;普通读路径不会因为平台角色自动穿透 private / hidden / unpublished

补充:

  • hidden=true可读权限会收敛为“skill owner 或 namespace ADMIN / OWNER
  • visibility=PUBLIC 也不意味着未发布 skill 可见;当 latest_version_id 为空时,只有 owner 能读

1.3.2 Version 状态读取

场景 / 角色 DRAFT PENDING_REVIEW PUBLISHED REJECTED YANKED
普通 skill 详情页主版本投影 不展示 owner / namespace 管理者可作为 ownerPreviewVersion 展示 展示 不展示 不展示
普通 listVersions 访客 不可见 不可见 可见 不可见 不可见
listVersions 的 owner / namespace ADMIN / OWNER 可见 可见 可见 可见 可见
常规 getVersionDetail 不可读 仅 owner 可读 可读 不可读 不可读
下载 / resolve / tag / 文件读取 不可用 不可用 可用 不可用 不可用
review 详情页 可见完整快照 可见完整快照 可见完整快照 可见完整快照 可见完整快照

补充:

  • YANKED 版本仍出现在管理视角的版本列表中,但不可下载
  • yank 当前最新已发布版本时,会重算 latest_version_id 指向下一个最新的 PUBLISHED 版本;若没有,则置空

1.3.3 审核 / 推广 / 治理动作

角色 发布新版本 提交审核 审核团队空间 审核全局空间 提交推广 审核推广 hide / unhide yank 已发布版本
匿名用户 不可 不可 不可 不可 不可 不可 不可 不可
namespace MEMBER 可发布到所属 namespace新版本进入 PENDING_REVIEW 自己作为 owner 时可;不能代别人提审 不可 不可 自己作为 owner 时可 不可 不可 不可
skill owner 不可 不可 不可 不可 不可
namespace ADMIN / OWNER 可为本空间 skill 提交审核 不可 不可 不可 不可
SKILL_ADMIN 可提交并可代提审;但普通发布仍非直发 可,但不能审自己的 promotion 不可
SUPER_ADMIN 可跨 namespace 发布且直接 PUBLISHED,跳过 membership 检查和 review task promotion 和 review 场景下还能审自己的提交

对象存储写入策略

一期同步写入正式路径,不使用临时区:

  • 文件直接写入 skills/{skillId}/{versionId}/{filePath}
  • 如果数据库事务失败,对象存储中的文件成为孤儿对象
  • 定时 GC 任务:每天扫描对象存储中存在但数据库中无对应 skill_file 记录的文件,清理孤儿对象
  • 删除 DRAFT/REJECTED 版本时,同步清理对应的对象存储文件

CLI publish 请求规范

POST /api/v1/publish
Content-Type: multipart/form-data
Parts:
  - file: zip 包(必需)
  - namespace: 目标命名空间 slug必需

一期同步响应:服务端同步完成上传、校验、存储、持久化,返回 200 OK + skill_version 信息。

当前 CLI 默认行为:上传 → 进入审核。 如果调用方持有 SUPER_ADMIN,则直接发布为 PUBLISHED。 Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确的审核提示。

/api/v1/publish 响应:

{
  "data": {
    "skillId": 456,
    "skillVersionId": 123,
    "version": "1.2.0",
    "status": "PUBLISHED",
    "namespace": "team-name",
    "slug": "my-skill"
  }
}

2 团队技能提升到全局空间(派生发布)

不直接修改原 skill 的 namespace_id,而是在全局空间创建新的 skill保留来源追溯。原团队 skill 继续存在,安装坐标 @team/skill 不受影响。

团队空间技能(已发布)
    │
    ▼
① 技能 owner 或 namespace admin 发起"提升到全局"申请
    │
    ▼
② 创建 promotion_request (source_skill_id, source_version_id, target_namespace_id, status=PENDING)
    │
    ▼
③ 平台管理员审核
   ├── 通过 →
   │   ① 在全局空间创建新 skillsource_skill_id = 原 skill ID
   │   ② 复制 source_version_id 对应版本的文件和元数据到新 skill严格使用申请时指定的版本不取最新
   │   ③ 新 skill.visibility = PUBLIC
   │   ④ promotion_request.target_skill_id = 新 skill IDstatus → APPROVED
   │   ⑤ 搜索索引写入新 skill同步写入审计日志
   │   (提升关系唯一事实来源是 promotion_requestUI 查询"是否已提升"通过该表判定)
   │
   └── 拒绝 → 记录原因,原技能不受影响

后续版本更新:

  • 全局空间的新 skill 由其 owner 独立管理版本
  • 原团队 skill 可继续独立迭代
  • 两者版本不自动同步,如需同步由 owner 手动操作

提升流程当前严格绑定已发布版本:

  • promotion request 的 source_version_id 必须指向 publishedVersion.id
  • 不允许直接提升 ownerPreviewVersion

3 下载流程

下载请求
    │
    ▼
① 校验技能状态ACTIVE、版本状态PUBLISHED
    │
    ▼
② 可见性检查
   - PUBLIC: 任何人(包括匿名用户)
   - NAMESPACE_ONLY: 该 namespace 的成员(需登录)
   - PRIVATE: owner 本人 + 该 namespace 的 ADMIN 以上(需登录)
    │
    ▼
③ 返回预生成包或按文件清单打包
    │
    ▼
④ 审计与统计
   - audit_log 同步写入(记录下载人/IP/版本)
   - download_count 异步更新(原子 SQL: download_count = download_count + 1
   - 匿名下载:审计记录 IP + User-Agent不关联用户
   - 已登录下载:审计记录用户 ID

download_count 热点行优化预案

一期使用原子 SQL 直接更新,可接受。如出现热点行瓶颈,切换为:

  1. Redis INCR 做实时计数key: skill:downloads:{skillId}
  2. 定时任务每 5 分钟批量回写 PostgreSQL
  3. 查询时合并 PostgreSQL 存量 + Redis 增量

4 搜索流程

搜索请求 (keyword, namespaceSlug?, sortBy)
    │
    ▼
① 构建 SearchQuery
   - 匿名用户visibility 限定为 PUBLIC
   - 已登录用户:根据命名空间成员关系计算可见范围
    │
    ▼
② SearchQueryService.search(query)
    │
    ▼
③ 返回分页结果(技能摘要 + 命名空间信息 + 评分 + 下载量)

5 收藏流程

收藏/取消收藏(需登录)→ 校验权限 → 写入/删除 skill_star
→ 异步更新 skill.star_count原子 SQL

6 评分流程

提交评分 (score: 1-5)(需登录)→ 校验权限 → 写入/更新 skill_rating
→ 异步重算 skill.rating_avg 和 rating_countSELECT AVG + Redis 分布式锁防重复重算)

7 异步事件汇总

事件 触发时机 消费方
SkillPublishedEvent 审核通过 搜索索引写入
SkillYankedEvent 版本撤回 搜索索引移除
SkillDownloadedEvent 下载完成 下载计数
SkillStarredEvent 收藏/取消 收藏计数
SkillRatedEvent 评分提交 评分重算
ReviewCompletedEvent 审核完成 预留给后续通知能力(当前可不消费)
SkillPromotedEvent 提升到全局 搜索索引写入(新 skill

一期用 Spring ApplicationEvent + @Async 实现,后续可替换为消息队列。

审计日志写入策略

审计日志统一同步落库,与业务操作在同一请求内同步写入,不走异步事件。审计是企业内部平台的刚性需求,不可容忍丢失。

异步事件仅用于搜索索引、计数器等可容忍延迟的场景。如果后续需要更强一致性,引入 outbox 模式,不依赖 ApplicationEvent + @Async 承担可靠性。

异步事件可靠性保障

Spring ApplicationEvent + @Async 存在 Pod 被杀时事件丢失的风险。补充以下兜底机制:

  • 搜索索引:定时任务每小时检查 skill_version.status = PUBLISHEDskill_search_document 中无对应记录的版本,补建索引
  • 计数器:可接受少量丢失,定时任务每天凌晨从 skill_star / skill_rating 表重算修正
  • 优雅停机:@Async 线程池配置 awaitTerminationSeconds=25,配合 30s shutdown timeout

8 分布式并发安全措施

操作 并发控制方式
审核通过/拒绝 乐观锁:UPDATE review_task SET status=? WHERE id=? AND version=?
版本发布 唯一约束:(skill_id, version)
计数器更新 原子 SQLSET count = count + 1
评分重算 异步 + Redis 分布式锁防重复重算
写操作幂等 Redis 存储 X-Request-IdTTL 24h

幂等去重规范

基于 idempotency_record 表实现完整幂等:

  • X-Request-Id 由客户端生成UUID v4 格式)
  • 客户端不传时,服务端自动生成但不做幂等去重

去重流程:

  1. Redis SETNX key=idempotent:{requestId}快速去重缓存TTL=24h
    • key 已存在:查询 idempotency_record 表返回原始结果
  2. key 不存在:插入 idempotency_recordstatus=PROCESSING
  3. 执行业务逻辑
  4. 成功:更新 record 为 COMPLETED,填充 resource_type + resource_id + response_status_code
  5. 失败:更新 record 为 FAILED
  6. 重复请求时:查 recordCOMPLETED 返回原始资源 IDPROCESSING 返回 409 ConflictFAILED 允许重试

适用范围:所有 POST/PUT/DELETE 写操作(发布、提审、创建 Token 等)

异常恢复策略:

  • Redis key 存在但 idempotency_record 无记录(进程在两步之间崩溃):视为脏状态,删除 Redis key允许请求正常重入
  • idempotency_record.status = FAILED:删除对应 Redis key允许客户端用相同 request_id 重试
  • idempotency_record.status = PROCESSING 超过 5 分钟未更新:视为僵死,标记为 FAILED删除 Redis key允许重试