skillhub 领域模型与数据模型
0. 用户标识约束
- 用户身份主键全链路统一为
string。
- 本约束覆盖
user_id、owner_id、created_by、updated_by、published_by、reviewed_by、actor_user_id 及所有等价语义字段。
- 历史文档里写成
bigint / BIGINT 的用户关联字段均应按字符串重新解释;这些旧类型描述不再作为实现依据。
- 若未来数据库为了索引或存储效率引入内部 surrogate key,也只能作为内部实现细节,不能替代字符串
userId 成为认证、授权、审计和 API 契约的主键。
3.1 核心实体
namespace
| 字段 |
类型 |
说明 |
| id |
bigint |
主键 |
| slug |
varchar(64) |
URL 友好标识 |
| display_name |
varchar(128) |
展示名 |
| type |
enum |
GLOBAL / CATALOG / TEAM |
| description |
text |
描述 |
| avatar_url |
varchar(512) |
头像 |
| status |
enum |
ACTIVE / FROZEN / ARCHIVED |
| created_by |
varchar(128) |
创建人 |
| created_at |
datetime |
|
| updated_at |
datetime |
|
GLOBAL 类型全局唯一(只有一个 @global),由平台管理员管理
CATALOG 为系统管理的来源目录;首个 @github 通过迁移创建,普通用户不能修改或删除
@github 迁移同时创建不可登录的系统 actor github-sync-publisher;若 namespace slug 或 actor ID 已被占用,迁移明确失败而不接管已有记录
TEAM 类型对应部门/团队,可创建多个
- 技能完整寻址:
@{namespace_slug}/{skill_slug}
- slug 唯一约束:
slug
- slug 格式校验:
[a-z0-9]([a-z0-9-]*[a-z0-9])?,长度 2-64,且不得包含连续两个以上的连字符 --(为兼容层坐标映射保留)
- slug 保留词列表(用户创建 namespace 时不可使用):
admin, api, dashboard, search, auth, me, global, github, system, static, assets, health;github 仅在 namespace 创建入口保留,不禁止同名 Skill
- 系统内置 namespace(
@global)在数据库初始化时由 Flyway 脚本预置,绕过 slug 校验规则。保留词校验仅作用于用户创建 namespace 的接口
- 状态语义:
ACTIVE:正常使用
FROZEN:冻结,只读不可发布新版本,已有技能仍可浏览/下载
ARCHIVED:归档,对外不可见
namespace_member
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| namespace_id |
bigint |
|
| user_id |
varchar(128) |
|
| role |
enum |
OWNER / ADMIN / MEMBER |
| created_at |
datetime |
|
| updated_at |
datetime |
|
OWNER:命名空间创建者,可转让
ADMIN:可审核该空间内的技能发布、管理成员
MEMBER:可在该空间内发布技能(提交审核)
- 唯一约束:
(namespace_id, user_id),一个用户在一个空间只有一个角色
skill
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| namespace_id |
bigint |
所属命名空间 |
| slug |
varchar(128) |
URL 友好标识 |
| display_name |
varchar(256) |
|
| summary |
varchar(512) |
|
| owner_id |
varchar(128) |
当前技能所有者;不等同于命名空间 OWNER,当前无独立技能转让入口 |
| source_skill_id |
bigint |
派生来源(团队技能提升到全局时记录原 skill ID),nullable |
| visibility |
enum |
PUBLIC / NAMESPACE_ONLY / PRIVATE |
| status |
enum |
ACTIVE / ARCHIVED |
| latest_version_id |
bigint |
内部生命周期指针;PRIVATE 上传可指向未发布版本。对外最新发布版本由 publishedVersion 解析,不直接以此字段授权 |
| download_count |
bigint |
|
| star_count |
int |
|
| rating_avg |
decimal(3,2) |
平均评分 |
| rating_count |
int |
评分人数 |
| created_by |
varchar(128) |
|
| created_at |
datetime |
|
| updated_by |
varchar(128) |
|
| updated_at |
datetime |
|
- 唯一约束:
(namespace_id, slug, owner_id);面向公众的同名冲突还须按已发布记录检查不同 owner,避免地址解析歧义
status 表示 skill 容器生命周期,不再承载“隐藏”语义。隐藏是独立的治理覆盖层,由 hidden / hidden_at / hidden_by 表达
- 容器可见性判定以
VisibilityChecker 为准;传入 SUPER_ADMIN 平台角色时该检查短路放行,但不表示所有调用方都传入角色,也不绕过调用方的其他限制。普通调用规则如下:
- 若
hidden=true:仅 skill owner 或该 namespace 的 ADMIN / OWNER 可读
- 若
latest_version_id is null:仅 skill owner 可读;即使 visibility=PUBLIC 也不会对外公开
PUBLIC:任意人可读 skill 容器与已发布版本
NAMESPACE_ONLY:该 namespace 任意成员及当前技能 owner 可读;owner 被移出 TEAM 空间后保留自己内容的只读访问
PRIVATE:仅 skill owner 或该 namespace 的 ADMIN / OWNER 可读,普通 MEMBER 不可读
owner_id 表示当前技能所有者,空间角色与技能所有权分别参与授权:
- 正常账号且仍为 TEAM 空间成员的 owner 按既有规则维护自己的 skill。
- 被移出 TEAM 空间但仍是当前 owner 时,可以查看、下载自己的内容,不能凭 owner 身份继续写入;不自动转移技能或所有权。
- 账号正常、重新加入且仍为 owner 时恢复正常维护权限;停用账号继续服从认证层限制。
- namespace ADMIN/OWNER 与平台角色的既有独立治理、审核权限保持不变,不因本条成为内容作者,也不新增全员覆盖能力。
- 历史提交人不是当前 owner 时,按普通可见性与已有角色判断访问,不获得永久私有内容读取权。
rating_avg / rating_count 冗余字段,避免每次查询聚合
slug:面向用户的 URL 标识,来自 SKILL.md 的 name 字段,首次发布后不可变更。slug 格式校验规则与 namespace slug 相同:[a-z0-9]([a-z0-9-]*[a-z0-9])?,同样适用保留词限制,且不得包含连续两个以上的连字符 --(为兼容层坐标映射保留)。全局空间(@global)下的 skill slug 额外禁止包含 --,以避免与兼容层 canonical slug 产生歧义
source_skill_id:仅在"团队技能提升到全局"场景下填充,记录原始团队空间的 skill ID,用于追溯来源
- 提升关系的唯一事实来源是
promotion_request 表。当前有效全局目标由 request_kind='INITIAL' AND status='APPROVED' AND target_skill_id IS NOT NULL 的首次提升记录确定;后续更新记录指向同一目标,撤销时解除目标引用并保留快照。
skill_version
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| skill_id |
bigint |
|
| version |
varchar(32) |
版本标识;可来自 SKILL.md 或系统生成,不按字符串大小判断“最新” |
| version_sort |
bigint |
排序用数值 |
| changelog |
text |
|
| manifest_json |
json |
文件清单 |
| parsed_metadata_json |
json |
SKILL.md frontmatter 解析结果 |
| status |
enum |
DRAFT / SCANNING / SCAN_FAILED / UPLOADED / PENDING_REVIEW / PUBLISHED / REJECTED / YANKED |
| reject_reason |
varchar(512) |
拒绝原因 |
| published_by |
varchar(128) |
|
| published_at |
datetime |
|
| created_at |
datetime |
|
status 表示 version 发布生命周期,和 skill 容器状态、review task 状态分离
- 完整状态定义、上传/扫描/审核转换和治理操作以 生命周期文档 为准;
DRAFT 保留为旧数据兼容状态,当前撤回审核回到 UPLOADED。
- 上传结果取决于请求可见性、扫描配置与发布权限,不能统一描述为“普通用户直接待审”或“超管发布即代表可安装”。
- 唯一约束:
(skill_id, version) 防止重复版本记录;已发布版本不可原地覆盖。允许删除的未发布版本删除后可复用版本号,但不能删除技能的最后一个版本。
YANKED 表示已发布后撤回,版本记录和版本号仍保留,不可下载。
- 版本列表、详情/文件预览、下载与公共分发是不同权限路径:管理者能预览的版本不一定能下载,
UPLOADED / PENDING_REVIEW 的受限下载也不代表对外发布。具体边界见 生命周期文档的对外协议与权限章节。
- 审核详情使用独立 review 读路径,可查看审核快照;不能由普通详情的访问规则推导审核权限。
skill_file
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| skill_version_id |
bigint |
|
| file_path |
varchar(512) |
|
| content_type |
varchar(128) |
|
| size_bytes |
bigint |
|
| sha256 |
varchar(64) |
|
| object_key |
varchar(512) |
|
| is_entry_file |
boolean |
|
| created_at |
datetime |
|
skill_tag
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| skill_id |
bigint |
|
| tag_name |
varchar(64) |
|
| target_version_id |
bigint |
|
| created_by |
varchar(128) |
|
| created_at |
datetime |
|
| updated_by |
varchar(128) |
|
| updated_at |
datetime |
|
latest 是系统保留标签,不允许 API 手动移动。当前标签列表在读取授权后按原始 latest_version_id 动态合成,不检查目标版本状态,因此 PRIVATE 上传的未发布指针也可能反映到授权用户的列表中;标签存在不等于可公开分发,下载仍需通过授权、版本状态与就绪检查
- 自定义标签(如
beta、stable-2026q1)允许人工创建和移动
- 唯一约束:
(skill_id, tag_name)
target_version_id 必须指向 status = PUBLISHED 的版本,应用层校验
review_task
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| skill_version_id |
bigint |
关联的版本 |
| namespace_id |
bigint |
所属空间(决定谁能审核) |
| status |
enum |
PENDING / APPROVED / REJECTED |
| version |
int |
乐观锁版本号,默认 1 |
| submitted_by |
varchar(128) |
提交人 |
| reviewed_by |
varchar(128) |
审核人 |
| review_comment |
text |
审核意见 |
| submitted_at |
datetime |
|
| reviewed_at |
datetime |
|
- 仅用于普通发布审核,"提升到全局"使用独立的
promotion_request 表
version 字段用于乐观锁,防止多 Pod 并发审核
- 业务约束:同一
skill_version_id 在 status=PENDING 时只能存在一条记录,重复提交返回 409 Conflict。撤回时删除 PENDING review_task,并将 skill_version 回退到 UPLOADED
- PostgreSQL 并发约束落地:通过唯一索引
(skill_version_id) + 软删除标记实现。review_task 表增加 deleted 字段(bigint, 默认 0),唯一索引改为 (skill_version_id, deleted)。撤回时将 deleted 设为 id(非零值),新提交时 deleted=0,利用唯一索引防止并发重复提交。或者采用更简单的方案:撤回时物理删除 review_task 记录,依赖 INSERT 的唯一约束 (skill_version_id) 防并发。PostgreSQL 还支持 partial unique index 方案:CREATE UNIQUE INDEX ON review_task (skill_version_id) WHERE status = 'PENDING',更优雅地实现"PENDING 状态唯一"约束
promotion_request
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| source_skill_id |
bigint |
来源团队 skill |
| source_version_id |
bigint |
申请提升的版本 |
| target_namespace_id |
bigint |
目标全局 namespace |
| target_skill_id |
bigint |
审批通过后生成的全局 skill ID,nullable |
| request_kind |
varchar(16) |
INITIAL / UPDATE;存量记录迁移为 INITIAL |
| target_version_id |
bigint |
审批形成的全局版本 ID 快照,允许目标撤销后保留 |
| target_skill_id_snapshot |
bigint |
撤销后仍保留原全局 skill ID |
| target_version_id_snapshot |
bigint |
撤销后保留结果版本 ID |
| revoked_at / revoked_by |
datetime / varchar(128) |
撤销生效时间和操作者 |
| status |
enum |
PENDING / APPROVED / REJECTED |
| version |
int |
乐观锁版本号,默认 1 |
| submitted_by |
varchar(128) |
提交人 |
| reviewed_by |
varchar(128) |
审核人 |
| review_comment |
text |
审核意见 |
| submitted_at |
datetime |
|
| reviewed_at |
datetime |
|
- 完整表达"哪个团队 skill 的哪一版被申请提升到哪个全局空间"。首次提升新建目标 skill;后续 UPDATE 在同一目标下增加不可变版本,两次发布审核分开进行。
- 审批通过后填充
target_skill_id,指向当前关联的全局 skill;撤销后清空该外键并保留快照和提升历史。
promotion_request 是提升关系的唯一事实来源,skill 表不再冗余 promoted_to_skill_id
- 业务约束:同一来源 skill 同时最多有一条
PENDING 提升申请;同一来源同时最多有一条有效 INITIAL 提升关系。全局目标有任何同号版本时,不允许 UPDATE 审批覆盖;异号审批按实际发布时间更新 latest。
- PostgreSQL 以
source_skill_id WHERE status='PENDING' 和 source_skill_id WHERE request_kind='INITIAL' AND status='APPROVED' AND target_skill_id IS NOT NULL 两个部分唯一索引防止并发重复。
promotion_revocation_request
- 来源技能所有者或团队管理员可提交撤销,平台管理员审核;平台管理员直接撤销也留存申请和审计。
- 审核通过时删除全局派生 skill 及其版本、统计和公开入口,不把数据迁回团队 skill;团队原件和共用的文件对象保留。
- 记录保留原始来源/目标 ID 与坐标、提交人、审核人、理由、状态及时间。目标全局 skill 物理删除后,这些字段仍供管理员审计。
- 撤销释放旧全局坐标;重新提升须重新申请、审核,并再次检查同名冲突。此前已下载的客户端文件无法收回。
skill_star
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| skill_id |
bigint |
|
| user_id |
varchar(128) |
|
| created_at |
datetime |
|
唯一约束:(skill_id, user_id)
skill_rating
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| skill_id |
bigint |
|
| user_id |
varchar(128) |
|
| score |
tinyint |
1-5 |
| review_text |
varchar(2000) |
可选文字评价;空值表示仅评分 |
| review_status |
enum |
VISIBLE / HIDDEN,隐藏不影响评分聚合 |
| moderated_by |
varchar(128) |
最近一次管理操作人,nullable |
| moderated_at |
datetime |
最近一次管理时间,nullable |
| moderation_reason |
varchar(500) |
隐藏原因,nullable |
| lock_version |
bigint |
乐观锁版本;并发编辑或治理冲突返回 409 |
| created_at |
datetime |
|
| updated_at |
datetime |
|
唯一约束:(skill_id, user_id),每人每技能一条,可修改。删除文字评价只清空
review_text,保留评分和既有治理状态;管理员隐藏评价时也保留评分,避免作者通过
清空后重新提交绕过治理,或治理动作改变聚合分数。
user_account
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| display_name |
varchar(128) |
|
| email |
varchar(256) |
|
| avatar_url |
varchar(512) |
|
| status |
enum |
ACTIVE / PENDING / DISABLED / MERGED |
| merged_to_user_id |
varchar(128) |
合并目标用户 ID,仅 MERGED 状态有值 |
| system_account |
boolean |
系统服务账号,禁止交互式 Web/OAuth 登录 |
| created_at |
datetime |
|
| updated_at |
datetime |
|
- 状态语义:
ACTIVE:正常使用
PENDING:等待管理员审批(AccessPolicy 返回 PENDING_APPROVAL 时创建)
DISABLED:管理员封禁,登录后拒绝所有操作,返回 403
MERGED:已合并到其他账号,保留记录不物理删除;登录直接拒绝,不向调用方泄露合并目标
- 授权层在每次请求时检查用户状态,非
ACTIVE 用户拒绝所有写操作
- system account 可按独立 Token Policy 使用非交互凭证,但不能通过本地密码或外部 OAuth
建立普通用户 Session
identity_binding
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| user_id |
varchar(128) |
|
| provider_code |
varchar(64) |
如 github |
| subject |
varchar(256) |
OAuth Provider 返回的唯一用户标识 |
| login_name |
varchar(128) |
如 GitHub login |
| extra_json |
json |
原始扩展字段 |
| created_at |
datetime |
|
| updated_at |
datetime |
|
- 唯一约束:
(provider_code, subject)
- 一期只接入 GitHub OAuth,但表结构支持后续扩展多个 OAuth Provider
api_token
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| subject_type |
varchar(32) |
USER(一期)/ SERVICE_ACCOUNT(预留) |
| subject_id |
varchar(128) |
关联主体 ID(一期等同于 user_id) |
| user_id |
varchar(128) |
兼容字段,一期与 subject_id 相同 |
| name |
varchar(128) |
Token 名称(必填),如"CI/CD"、"本地开发" |
| token_prefix |
varchar(16) |
|
| token_hash |
varchar(64) |
|
| scope_json |
json |
|
| expires_at |
datetime |
|
| last_used_at |
datetime |
|
| revoked_at |
datetime |
|
| created_at |
datetime |
|
audit_log
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| actor_user_id |
varchar(128) |
|
| action |
varchar(64) |
|
| target_type |
varchar(64) |
|
| target_id |
bigint |
|
| request_id |
varchar(64) |
|
| client_ip |
varchar(64) |
|
| user_agent |
varchar(512) |
|
| detail_json |
json |
|
| created_at |
datetime |
|
3.2 RBAC 实体
一期即上线完整 RBAC,平台角色按最小权限拆分,避免所有治理能力压在单一超管角色上。
平台角色(一期内置,Flyway 预置):
| 角色 code |
说明 |
典型权限 |
SUPER_ADMIN |
平台超管,拥有所有权限 |
全部 |
SKILL_ADMIN |
技能治理:全局空间审核、提升审核、隐藏/恢复、撤回已发布版本 |
review:approve, skill:manage, promotion:approve |
USER_ADMIN |
用户治理:准入审批、封禁/解封、角色分配(不可分配 SUPER_ADMIN) |
user:manage, user:approve |
AUDITOR |
审计只读:查看审计日志 |
audit:read |
- 命名空间权限仍由
namespace_member.role(OWNER / ADMIN / MEMBER)决定,不走 RBAC 表
- 一个用户可持有多个平台角色(多条
user_role_binding)
SUPER_ADMIN 隐含所有权限,代码中硬判定短路
role
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| code |
varchar(64) |
SUPER_ADMIN / SKILL_ADMIN / USER_ADMIN / AUDITOR |
| name |
varchar(128) |
展示名 |
| description |
varchar(512) |
|
| is_system |
boolean |
系统内置角色不可删除 |
| created_at |
datetime |
|
permission
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| code |
varchar(128) |
如 skill:publish, review:approve, user:manage |
| name |
varchar(128) |
|
| group_code |
varchar(64) |
权限分组 |
role_permission
| 字段 |
类型 |
说明 |
| role_id |
bigint |
|
| permission_id |
bigint |
|
user_role_binding
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| user_id |
varchar(128) |
|
| role_id |
bigint |
|
| created_at |
datetime |
|
3.3 搜索文档表
skill_search_document
一个 skill 对应一条搜索文档,内容取 publishedVersion 表示的“最新已发布版本”。内部 latest_version_id 可能在 PRIVATE 上传时指向未发布版本,不能不校验状态就用于公开搜索。
| 字段 |
类型 |
说明 |
| id |
bigint |
|
| skill_id |
bigint |
唯一,一 skill 一条 |
| namespace_id |
bigint |
用于空间过滤 |
| owner_id |
varchar(128) |
用于 PRIVATE 可见性判定 |
| title |
varchar(256) |
|
| summary |
varchar(512) |
|
| keywords |
varchar(512) |
|
| search_text |
text |
displayName、slug、summary,以及 frontmatter 中除 name / description / version 外的字段展开结果 |
| visibility |
enum |
冗余,避免搜索时 join |
| status |
enum |
|
| updated_at |
datetime |
|
PostgreSQL Full-Text Index:在 skill_search_document 表增加 search_vector tsvector 列,通过触发器或 GENERATED ALWAYS AS 自动维护,建立 GIN 索引。
3.4 幂等记录表
idempotency_record
| 字段 |
类型 |
说明 |
| request_id |
varchar(64) |
主键,客户端传入的 UUID v4 |
| resource_type |
varchar(64) |
如 skill_version, api_token |
| resource_id |
bigint |
业务操作产生的资源 ID |
| status |
enum |
PROCESSING / COMPLETED / FAILED |
| response_status_code |
int |
原始响应状态码 |
| created_at |
datetime |
|
| expires_at |
datetime |
过期时间(默认 24h) |
- 流程:收到请求 → 插入 record(PROCESSING)→ 业务处理 → 更新为 COMPLETED + resource_id → 重复请求时查 record 返回已有结果
- Redis 做快速去重缓存(SETNX),PostgreSQL 做持久化兜底
- 定时任务清理过期记录
3.5 关键索引设计
| 表 |
索引 |
用途 |
| namespace |
(slug) UNIQUE |
唯一约束 |
| skill |
(namespace_id, status) |
命名空间内技能列表 |
| skill |
(namespace_id, slug) UNIQUE |
唯一约束 |
| skill_version |
(skill_id, status) |
版本列表 |
| skill_version |
(skill_id, version) UNIQUE |
唯一约束 |
| skill_tag |
(skill_id, tag_name) UNIQUE |
标签唯一约束 |
| review_task |
(namespace_id, status) |
审核列表 |
| review_task |
(submitted_by, status) |
我的提交 |
| promotion_request |
(source_skill_id) |
按来源 skill 查询 |
| promotion_request |
(status) |
待审核列表 |
| idempotency_record |
(expires_at) |
过期清理 |
| audit_log |
(created_at) |
审计查询 |
| audit_log |
(actor_user_id, created_at) |
用户操作历史 |
| skill_star |
(user_id) |
我的收藏 |
| skill_star |
(skill_id) |
技能收藏数 |
| skill_rating |
(skill_id) |
评分聚合 |
| namespace_member |
(namespace_id, user_id) UNIQUE |
成员唯一约束 |
| namespace_member |
(user_id) |
用户所属空间 |
| identity_binding |
(provider_code, subject) UNIQUE |
身份查找 |
| api_token |
(token_hash) |
Token 校验 |