mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-24 00:55:35 +00:00
Align the label system design with click-through chips on skill detail and wrapping search filter chips when many labels are present. Signed-off-by: Mikhail Neradkov <michael.neradkov@gmail.com>
467 lines
20 KiB
Markdown
467 lines
20 KiB
Markdown
# Skill Label System Design
|
||
|
||
> Date: 2026-03-20
|
||
> Status: Draft
|
||
> Scope: Phase 1 — 系统推荐标签 + 特权标签
|
||
|
||
## 1. Overview
|
||
|
||
为 SkillHub 引入 label 系统,为 skill 提供分类和标记能力。Label 挂在 skill 级别(与版本无关),支持多语言展示和搜索。
|
||
|
||
注意:本系统使用 "label" 而非 "tag",因为 `skill_tag` 已被版本分发通道功能占用。
|
||
|
||
### 1.1 一期范围
|
||
|
||
**包含:**
|
||
- 系统推荐标签(RECOMMENDED):管理员 CRUD + 多语言翻译 + 排序,用于搜索页分类筛选
|
||
- 特权标签(PRIVILEGED):管理员专属赋予,如"官方推荐"、"官方认证"、"从Clawhub镜像"
|
||
- Skill 详情页 label 展示与管理
|
||
- 搜索页分类板块(单选互斥筛选)
|
||
- 多语言搜索命中(所有语言翻译写入搜索文档)
|
||
|
||
**不包含(保留兼容性):**
|
||
- 用户自定义标签
|
||
- 用户自定义标签审核流程
|
||
|
||
### 1.2 关键决策
|
||
|
||
- `label_*` 是全新模型,与现有 `skill_tag` 彻底隔离;`skill_tag` 继续只承担“版本分发别名”的职责,不复用表、Service、Controller、DTO、API 路径
|
||
- `skill_search_document.keywords` 是搜索文档的共享聚合字段,不是 label 专属字段;一期在搜索文档重建时,将“现有业务 keywords 来源”和“label 翻译文本”作为两个独立来源重新组合写入
|
||
- label 搜索集成只允许“基于权威源全量重建单个 skill 的搜索文档”,不允许读取现有 `skill_search_document.keywords` 后做增量 append
|
||
- promotion 在当前系统中会创建新的 target skill,而不是把 source skill 移动到新空间;因此 label 生命周期必须按“source skill / target skill 两条独立 skill 记录”建模
|
||
|
||
## 2. Data Model
|
||
|
||
注意:新表统一使用 `TIMESTAMPTZ` 作为时间戳类型标准(现有旧表使用 `TIMESTAMP`,后续统一迁移)。
|
||
|
||
### 2.1 label_definition(标签定义表)
|
||
|
||
```sql
|
||
CREATE TABLE label_definition (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
slug VARCHAR(64) UNIQUE NOT NULL, -- 英文标识,必填,如 code-generation
|
||
type VARCHAR(16) NOT NULL CHECK (type IN ('RECOMMENDED', 'PRIVILEGED')),
|
||
visible_in_filter BOOLEAN NOT NULL DEFAULT true, -- 是否在搜索页分类板块展示
|
||
sort_order INTEGER NOT NULL DEFAULT 0, -- 分类板块显示顺序
|
||
created_by VARCHAR(128) REFERENCES user_account(id),
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
```
|
||
|
||
- `slug` 即英文名称,作为语言无关的唯一标识
|
||
- `type` 区分系统推荐标签和特权标签,决定权限控制策略。DDL 层通过 CHECK 约束限制合法值;应用层权限校验对未知 type 采用 deny-by-default 策略
|
||
- `visible_in_filter` 控制是否出现在搜索页分类板块,RECOMMENDED 和 PRIVILEGED 均可配置
|
||
|
||
### 2.2 label_translation(标签翻译表)
|
||
|
||
```sql
|
||
CREATE TABLE label_translation (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
label_id BIGINT NOT NULL REFERENCES label_definition(id) ON DELETE CASCADE,
|
||
locale VARCHAR(16) NOT NULL, -- 语言代码,如 en、zh、ja
|
||
display_name VARCHAR(128) NOT NULL, -- 该语言的显示名称
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE(label_id, locale)
|
||
);
|
||
```
|
||
|
||
- 支持动态语言:管理员可为任意语言添加翻译,不限于系统当前支持的语言列表
|
||
- 前端展示 fallback 顺序:当前语言 → en → slug
|
||
- 后端返回 `displayName` 时,“当前语言”以请求 locale 为准;实现上使用 Spring locale 解析结果(等价于基于 `Accept-Language` / request locale),再 fallback 到 `en` 和 `slug`
|
||
|
||
### 2.3 skill_label(skill 与 label 关联表)
|
||
|
||
```sql
|
||
CREATE TABLE skill_label (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
skill_id BIGINT NOT NULL REFERENCES skill(id) ON DELETE CASCADE,
|
||
label_id BIGINT NOT NULL REFERENCES label_definition(id) ON DELETE CASCADE,
|
||
created_by VARCHAR(128) REFERENCES user_account(id),
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE(skill_id, label_id)
|
||
);
|
||
|
||
CREATE INDEX idx_skill_label_label_id ON skill_label(label_id);
|
||
```
|
||
|
||
- Label 挂在 skill 级别,与版本无关
|
||
- 级联删除:删除 label_definition 时自动清理关联
|
||
- `(label_id)` 索引用于分类筛选时按 label 查找关联 skill 的性能优化
|
||
- 单个 skill 最多关联 10 个 label(应用层校验)
|
||
|
||
### 2.5 与现有 `skill_tag` 的关系
|
||
|
||
现有 `skill_tag` 已用于版本分发通道,例如 `latest`、`beta` 等 tag 指向某个发布版本。它具备以下特征:
|
||
- 语义是“版本别名”,不是 skill 分类
|
||
- `version_id` 必填,tag 必须解析到某个 skill version
|
||
- API 和前端心智都已围绕“安装/下载某个版本别名”展开
|
||
|
||
因此:
|
||
- 新 label 系统不得复用 `skill_tag` 表结构
|
||
- 新 label 系统不得复用 `/tags` 相关 API 路径
|
||
- 代码实现中必须使用独立的命名:`LabelDefinition` / `SkillLabel` / `LabelTranslation`
|
||
|
||
### 2.4 兼容性设计:用户自定义标签
|
||
|
||
未来用户自定义标签可通过以下方式扩展,无需新建表:
|
||
|
||
1. `label_definition.type` 增加 `USER_DEFINED` 枚举值
|
||
2. `label_definition` 增加字段:
|
||
- `status VARCHAR(16)` — `PENDING_REVIEW` / `APPROVED` / `REJECTED`,用于审核流程
|
||
- `submitted_by VARCHAR(128)` — 提交人
|
||
3. `label_translation` 对于用户自定义标签只需存储用户输入的原始语言,无需多语言翻译
|
||
4. 搜索行为:用户自定义标签以原始文本写入搜索文档 keywords,只能搜索用户输入的语言
|
||
|
||
这种设计保证了:
|
||
- 现有表结构无需破坏性变更
|
||
- 权限模型自然扩展(`USER_DEFINED` 类型有独立的权限规则)
|
||
- 搜索集成方式一致(均通过 keywords 字段)
|
||
|
||
## 3. Permission Model
|
||
|
||
| 操作 | 对象 | 超级管理员 | 命名空间管理员 | Skill Owner | 普通用户 |
|
||
|------|------|:---:|:---:|:---:|:---:|
|
||
| CRUD 标签定义 | label_definition | ✅ | ❌ | ❌ | ❌ |
|
||
| 管理翻译 | label_translation | ✅ | ❌ | ❌ | ❌ |
|
||
| 赋予/移除 RECOMMENDED label | skill_label | ✅ | ✅(本空间,仅限搜索页可见的 RECOMMENDED 标签) | ✅(自己的 skill,仅限搜索页可见的 RECOMMENDED 标签) | ❌ |
|
||
| 赋予/移除 PRIVILEGED label | skill_label | ✅ | ❌ | ❌ | ❌ |
|
||
| 查看 label | 所有表 | ✅ | ✅ | ✅ | ✅(受 skill 可见性约束) |
|
||
|
||
- 标签定义和翻译的管理是全局操作,仅超级管理员
|
||
- 赋予 label 到 skill 的权限取决于 label 的 type
|
||
- 查看权限跟随 skill 本身的可见性规则,不额外控制
|
||
- 实现上必须抽出统一的 `LabelPermissionChecker`;`SUPER_ADMIN` 始终可绕过 namespace membership 直接执行 label 管理操作,避免 controller / service 各自复制权限逻辑
|
||
|
||
### 3.1 跨空间权限边界
|
||
|
||
- 命名空间管理员只能管理其所管理空间内 skill 的 label
|
||
|
||
### 3.2 Promotion 后的 label 生命周期
|
||
|
||
当前 promotion 的事实模型是“审批后在目标全局空间创建一个新的 target skill”,而不是把 source skill 迁移到全局空间。
|
||
|
||
一期采用以下规则:
|
||
- promotion 不自动复制 source skill 的任何 label 到 target skill
|
||
- source skill 和 target skill 各自维护独立的 `skill_label`
|
||
- source 空间管理员对 source skill 的 label 权限不变
|
||
- target skill 的 label 由 target skill 当前权限模型控制;source 空间管理员不会因 source skill 的管理权限而自动获得 target skill 的 label 管理权
|
||
|
||
这样做的原因:
|
||
- 避免在一期引入“promotion 时 label 复制/回写/同步”的额外复杂度
|
||
- 与当前 promotion “创建新 skill 副本”的领域模型一致
|
||
- 后续如需复制策略,可在 promotion approval 流程中显式扩展,而不破坏现有表结构
|
||
|
||
## 4. Search Integration
|
||
|
||
采用翻译文本展开写入搜索文档方案。
|
||
|
||
### 4.1 搜索架构现状
|
||
|
||
当前搜索基于 PostgreSQL Full-Text Search:
|
||
- `skill_search_document` 表有 `search_vector` 列,类型为 `tsvector GENERATED ALWAYS AS ... STORED`
|
||
- 权重体系:title (A) > summary/keywords (B) > search_text (C)
|
||
- `search_vector` 在 keywords 等字段更新时自动重新生成,无需手动维护
|
||
- 查询时通过 `d.search_vector @@ to_tsquery('simple', :tsQuery)` 进行全文匹配
|
||
|
||
### 4.2 Keywords 字段写入
|
||
|
||
在构建 `SkillSearchDocument` 时,将 skill 关联的所有 label 的所有语言翻译文本写入 `keywords` 字段。
|
||
|
||
**重要:** `skill_search_document.keywords` 不是 label 专属字段,而是搜索文档的共享聚合字段。当前系统中,该字段已经承载来自 skill metadata/frontmatter 的 keywords/tag 信息。label 翻译文本只是新增来源之一,不能覆盖或破坏现有来源。
|
||
|
||
实现要求:
|
||
- 搜索文档重建时,从权威源重新计算完整的 `keywords`
|
||
- 现有业务 keywords 来源与 label 翻译文本作为两个独立来源进行组合
|
||
- 不允许读取旧的 `skill_search_document.keywords` 后做增量 append
|
||
- label 删除、翻译修改、skill 移除 label 后,旧 label 文本必须通过重建被彻底清理,不得残留
|
||
|
||
建议实现上的组合顺序:
|
||
1. 保留现有搜索重建逻辑产出的原有 keywords 内容
|
||
2. 追加该 skill 关联 label 的全部翻译文本
|
||
3. 最终统一写回新的 `SkillSearchDocument.keywords`
|
||
|
||
示例:
|
||
```
|
||
[原有 keywords 内容] Code Generation 代码生成 Official 官方推荐
|
||
```
|
||
|
||
### 4.3 搜索文档重建触发时机
|
||
|
||
| 事件 | 影响范围 | 处理方式 |
|
||
|------|---------|---------|
|
||
| Skill 被赋予/移除 label | 单个 skill | 同步重建该 skill 搜索文档 |
|
||
| label_translation 被修改 | 所有关联该 label 的 skill | 异步批量重建 |
|
||
| label_definition 被删除 | 所有关联该 label 的 skill | 异步批量重建 |
|
||
|
||
#### 异步批量重建方案
|
||
|
||
- 使用 Spring `@Async` 执行异步任务
|
||
- 在 `skillhub-app` 层的 application service 中实现 label 相关的搜索同步入口(不放在 `skillhub-search` 模块的 `SearchRebuildService` 中,避免搜索模块对 `skill_label` 表的直接依赖,保持模块边界清晰)
|
||
- 对 `label_translation` 修改、`label_definition` 删除等“影响多个 skill”的变更,在事务内先收集受影响的 `skill_id` 列表,再在 `AFTER_COMMIT` 阶段触发异步任务
|
||
- `label_definition` 删除场景严禁在删除后再通过 `skill_label` 反查,因为 `skill_label` 已被级联删除;必须在删除前快照受影响的 `skill_id`
|
||
- 异步任务分批调用 `SearchRebuildService.rebuildBySkill(Long)`
|
||
- 批量大小:每批 50 个 skill,批次间无需间隔(数据库写入压力可控,系统推荐标签数量有限)
|
||
- 失败隔离策略:批量重建循环中必须对每个 skill 单独 `try/catch` 并记录日志,保证单个 skill 失败不影响后续 skill
|
||
- 错误处理:单个 skill 重建失败记录错误日志,不自动重试(下次 label 变更或手动 rebuildAll 时会修复)
|
||
- 如单次受影响 skill 数过多,应允许后台人工触发搜索全量重建作为兜底手段
|
||
- 对“热门 label 导致大量 skill 批量重建”的场景,一期不单独引入任务表;优先依赖现有异步线程池执行,小规模批量直接处理,超大批量由后台人工触发 `rebuildAll` 兜底
|
||
|
||
### 4.4 分类筛选
|
||
|
||
搜索页分类板块的筛选不走全文搜索,而是通过 `skill_label` JOIN `label_definition` 按 slug 做大小写不敏感过滤,再与搜索结果取交集。避免全文搜索的模糊性问题。
|
||
|
||
当前搜索入口为:
|
||
```
|
||
GET /api/web/skills?q=xxx
|
||
```
|
||
|
||
一期在现有入口上增加可选 query parameter `label`,支持多值以预留未来组合筛选能力(一期前端只做单选):
|
||
```
|
||
GET /api/web/skills?q=xxx&label=code-generation
|
||
GET /api/web/skills?q=xxx&label=code-generation&label=official (未来)
|
||
```
|
||
|
||
#### SearchQuery 改动
|
||
|
||
`SearchQuery` 需要新增 `labelSlugs` 字段:
|
||
```java
|
||
public record SearchQuery(
|
||
String keyword,
|
||
Long namespaceId,
|
||
SearchVisibilityScope visibilityScope,
|
||
String sortBy,
|
||
int page,
|
||
int size,
|
||
List<String> labelSlugs
|
||
) {}
|
||
```
|
||
|
||
注意:
|
||
- 这不是“零成本追加字段”;当前 controller、application service、query service、测试代码都需要同步修改
|
||
- 实现时应显式梳理以下变更点:HTTP 参数解析、`SkillSearchAppService` 参数透传、`PostgresFullTextQueryService` SQL 条件、相关单元测试/控制器测试
|
||
- 若后续搜索过滤条件继续增加,应考虑把 `SearchQuery` 从位置参数 record 演进为更可扩展的请求对象
|
||
|
||
`PostgresFullTextQueryService` 的 SQL 拼接逻辑中,当 `labelSlugs` 非空时追加:
|
||
```sql
|
||
AND d.skill_id IN (
|
||
SELECT sl.skill_id FROM skill_label sl
|
||
JOIN label_definition ld ON ld.id = sl.label_id
|
||
WHERE ld.slug IN (:labelSlugs)
|
||
)
|
||
```
|
||
|
||
count 查询同步追加相同条件。语义重排在 label 过滤后的候选集上执行,无需额外处理。
|
||
|
||
当前多 label 筛选采用 OR 语义(匹配任一 label 即命中)。未来如需 AND 语义(同时具有所有 label),可通过 `GROUP BY skill_id HAVING COUNT(*) = :labelCount` 扩展,API 层增加 `labelMode=any|all` 参数区分。
|
||
|
||
### 4.5 tsvector 权重
|
||
|
||
不改变现有权重体系。`search_vector` 是 `GENERATED ALWAYS AS ... STORED` 列,keywords 字段更新后自动重新生成,无需手动维护:
|
||
- A 权重:title (displayName)
|
||
- B 权重:summary / keywords(含 label 翻译文本)
|
||
- C 权重:searchText
|
||
|
||
## 5. API Design
|
||
|
||
### 5.1 管理后台 API(超级管理员)
|
||
|
||
所有响应遵循项目统一响应规范 `{ code, msg, data, timestamp, requestId }`。
|
||
|
||
#### 列出所有标签定义
|
||
```
|
||
GET /api/v1/admin/labels
|
||
```
|
||
Response `data`:
|
||
```json
|
||
[
|
||
{
|
||
"slug": "code-generation",
|
||
"type": "RECOMMENDED",
|
||
"visibleInFilter": true,
|
||
"sortOrder": 10,
|
||
"translations": [
|
||
{ "locale": "en", "displayName": "Code Generation" },
|
||
{ "locale": "zh", "displayName": "代码生成" }
|
||
],
|
||
"createdAt": "2026-03-20T10:00:00Z"
|
||
}
|
||
]
|
||
```
|
||
不分页,系统标签数量有限(建议上限 100 个 label_definition)。
|
||
|
||
#### 创建标签定义
|
||
```
|
||
POST /api/v1/admin/labels
|
||
```
|
||
```json
|
||
{
|
||
"slug": "code-generation",
|
||
"type": "RECOMMENDED",
|
||
"visibleInFilter": true,
|
||
"sortOrder": 10,
|
||
"translations": [
|
||
{ "locale": "en", "displayName": "Code Generation" },
|
||
{ "locale": "zh", "displayName": "代码生成" }
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 更新标签定义
|
||
```
|
||
PUT /api/v1/admin/labels/{slug}
|
||
```
|
||
Body 不包含 slug 字段(slug 不可修改,以 path 参数为准):
|
||
```json
|
||
{
|
||
"type": "RECOMMENDED",
|
||
"visibleInFilter": true,
|
||
"sortOrder": 10,
|
||
"translations": [
|
||
{ "locale": "en", "displayName": "Code Generation" },
|
||
{ "locale": "zh", "displayName": "代码生成" }
|
||
]
|
||
}
|
||
```
|
||
translations 采用全量替换策略:请求中的 translations 列表完全替代现有翻译。如果删除了某个语言的翻译,会触发关联 skill 的异步搜索文档重建。
|
||
|
||
#### 删除标签定义
|
||
```
|
||
DELETE /api/v1/admin/labels/{slug}
|
||
```
|
||
硬删除。级联删除关联的 translations 和 skill_label 记录,触发异步搜索文档重建。删除操作会记录到 audit_log。
|
||
|
||
#### 批量更新排序
|
||
```
|
||
PUT /api/v1/admin/labels/sort-order
|
||
```
|
||
```json
|
||
{
|
||
"items": [
|
||
{ "slug": "code-generation", "sortOrder": 1 },
|
||
{ "slug": "official", "sortOrder": 2 }
|
||
]
|
||
}
|
||
```
|
||
|
||
### 5.2 Skill Label 管理 API
|
||
|
||
路由约定:
|
||
- 为与现有 skill read 接口风格保持一致,skill 详情读取类 label API 采用双路由暴露:`/api/v1/...` 与 `/api/web/...`
|
||
- 管理后台 label definition API 继续只暴露在 `/api/v1/admin/...`
|
||
- 搜索页所需的公开 labels 列表 API 一期同时暴露 `/api/v1/labels` 与 `/api/web/labels`,前端默认使用 `/api/web/labels`
|
||
|
||
#### 获取 skill 的所有 label
|
||
```
|
||
GET /api/v1/skills/{namespace}/{slug}/labels
|
||
GET /api/web/skills/{namespace}/{slug}/labels
|
||
```
|
||
Response `data`:
|
||
```json
|
||
[
|
||
{
|
||
"slug": "code-generation",
|
||
"type": "RECOMMENDED",
|
||
"displayName": "代码生成"
|
||
},
|
||
{
|
||
"slug": "official",
|
||
"type": "PRIVILEGED",
|
||
"displayName": "官方推荐"
|
||
}
|
||
]
|
||
```
|
||
`displayName` 根据请求语言返回,fallback 顺序:当前语言 → en → slug。
|
||
|
||
DTO 约定:
|
||
- 一期统一返回 `slug`、`type`、`displayName`
|
||
- 如后续需要区分视觉样式,可在前端基于 `type` 判断
|
||
|
||
#### 赋予 label
|
||
```
|
||
PUT /api/v1/skills/{namespace}/{slug}/labels/{labelSlug}
|
||
PUT /api/web/skills/{namespace}/{slug}/labels/{labelSlug}
|
||
```
|
||
权限校验:RECOMMENDED → owner / 命名空间管理员 / 超级管理员;PRIVILEGED → 仅超级管理员。
|
||
|
||
#### 移除 label
|
||
```
|
||
DELETE /api/v1/skills/{namespace}/{slug}/labels/{labelSlug}
|
||
DELETE /api/web/skills/{namespace}/{slug}/labels/{labelSlug}
|
||
```
|
||
权限校验同赋予。
|
||
|
||
### 5.3 公开查询 API
|
||
|
||
#### 获取可用标签列表(搜索页分类板块)
|
||
```
|
||
GET /api/v1/labels
|
||
GET /api/web/labels
|
||
```
|
||
返回 `visible_in_filter=true` 且 `type='RECOMMENDED'` 的标签,按 `sort_order` 排序。`PRIVILEGED` 一期不出现在搜索页分类筛选中,避免运营/特权标签与功能分类混淆。Response `data`:
|
||
```json
|
||
[
|
||
{
|
||
"slug": "code-generation",
|
||
"type": "RECOMMENDED",
|
||
"displayName": "代码生成"
|
||
}
|
||
]
|
||
```
|
||
`displayName` 根据请求语言返回,fallback 顺序:当前语言 → en → slug。不分页。
|
||
|
||
## 6. ClawHub 兼容层
|
||
|
||
ClawHub CLI 兼容层的搜索接口 `GET /api/v1/search` 一期不支持 label 筛选。ClawHub 协议中没有 label 概念,无需兼容。
|
||
|
||
## 7. Frontend Design
|
||
|
||
### 7.1 搜索页
|
||
|
||
- 搜索框下方增加分类板块,水平排列 label 列表(数据来自 `GET /api/v1/labels`);标签过多时应允许换行(`flex-wrap`),避免单行溢出
|
||
- 每个 label 显示当前语言的 display_name,fallback 顺序:当前语言 → en → slug
|
||
- 点击某个 label 高亮选中,搜索请求追加 `label` 参数;再次点击取消选中
|
||
- Label 之间单选互斥:点击另一个 label 切换选中,不支持组合筛选
|
||
- 选中状态通过 URL query parameter 同步,支持分享链接
|
||
|
||
### 7.2 Skill 详情页
|
||
|
||
- 在 skill 信息区域以 chip/badge 形式展示该 skill 的所有 label
|
||
- 特权标签使用不同的视觉样式区分(不同颜色或图标)
|
||
- 点击 chip 导航到搜索页并带上 `label=<slug>`(与 §7.1 同一筛选语义,便于从详情发现同标签技能);默认清空关键词、`sort=newest`、`page=0`
|
||
- 若该 slug 不在搜索页可见筛选列表中(例如 `visible_in_filter=false` 的 PRIVILEGED),URL 仍可携带 `label` 并生效,但筛选条上可能没有对应高亮按钮
|
||
- 有权限的用户(owner / 命名空间管理员 / 超级管理员)看到编辑入口
|
||
- 编辑交互:弹出面板;超级管理员可从全部 label definition 中勾选/取消勾选,owner / 命名空间管理员仅可操作搜索页可见的 RECOMMENDED 标签
|
||
- 特权标签区域仅超级管理员可见和可操作
|
||
|
||
补充说明:
|
||
- 这不是仅靠新增独立 label API 就能完成的能力,skill detail DTO / OpenAPI / 前端类型 / 详情页查询链路都需要增加 labels 字段
|
||
- 建议 skill 详情首屏直接返回 labels,避免详情页再额外发起一次 label 查询导致展示和权限状态碎片化
|
||
- 一期仅要求 `SkillDetailResponse` 增加 `labels: List<SkillLabelDto>`;`SkillSummaryResponse` 暂不增加 labels,保持搜索结果与列表卡片改动最小
|
||
- `SkillLabelDto` 字段固定为 `slug`、`type`、`displayName`
|
||
|
||
### 7.3 管理后台
|
||
|
||
- 标签管理页面:列表展示所有标签定义,支持拖拽排序
|
||
- 创建/编辑标签:表单包含 slug(创建时填写,不可修改)、type 选择、visible_in_filter 开关,以及动态翻译条目(可添加任意语言的翻译)
|
||
- 删除标签需二次确认,提示会影响已关联的 skill
|
||
|
||
## 8. Testing
|
||
|
||
一期至少补充以下测试:
|
||
- `PostgresSearchRebuildService`:验证原有 keywords 来源与 label translations 的组合结果
|
||
- `PostgresSearchRebuildService`:验证 label 删除/翻译修改后,旧 label 词不会残留
|
||
- `PostgresFullTextQueryService`:验证 `labelSlugs` 过滤 SQL 生效,且 count 查询同步生效
|
||
- `SkillSearchController` / `SkillSearchAppService`:验证 `label` 参数透传
|
||
- promotion 相关测试:验证 source skill 与 target skill 的 labels 独立,不发生隐式复制
|
||
|
||
## 9. Audit
|
||
|
||
以下动作需记录到 `audit_log`,供后台追踪:
|
||
- `LABEL_CREATE`
|
||
- `LABEL_UPDATE`
|
||
- `LABEL_DELETE`
|
||
- `LABEL_SORT_ORDER_UPDATE`
|
||
- `SKILL_LABEL_ATTACH`
|
||
- `SKILL_LABEL_DETACH`
|