Install
npx skillscat add jd-opensource/joysafeter/backend-app Install via the SkillsCat registry.
SKILL 后端功能说明文档
概述
SKILL(技能)模块是平台的核心功能之一,用于管理和组织可复用的技能资源。每个技能可以包含描述、内容、标签、文件等丰富的信息,支持公开分享和私有管理。
架构设计
分层架构
SKILL 模块采用经典的分层架构设计:
API Layer (api/v1/skills.py)
↓
Service Layer (services/skill_service.py)
↓
Repository Layer (repositories/skill.py)
↓
Model Layer (models/skill.py)核心组件
模型层 (Models)
Skill: 技能主表模型SkillFile: 技能文件关联表模型
仓库层 (Repositories)
SkillRepository: 技能数据访问层SkillFileRepository: 技能文件数据访问层
服务层 (Services)
SkillService: 技能业务逻辑和权限校验
API层 (API)
- RESTful API 端点,提供完整的 CRUD 操作
数据模型
Skill 模型
技能主表,存储技能的核心信息:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
UUID | 主键,自动生成 |
name |
String(255) | 技能名称,必填 |
description |
Text | 技能描述,必填 |
content |
Text | 技能内容,必填 |
tags |
JSONB | 标签列表,默认为空列表 |
source_type |
String(50) | 来源类型,默认为 "local" |
source_url |
String(1024) | 来源 URL,可选 |
root_path |
String(512) | 根路径,可选 |
owner_id |
String(255) | 拥有者 ID,外键关联 user.id |
created_by_id |
String(255) | 创建者 ID,外键关联 user.id,必填 |
is_public |
Boolean | 是否公开,默认为 False |
license |
String(100) | 许可证信息,可选 |
created_at |
DateTime | 创建时间 |
updated_at |
DateTime | 更新时间 |
约束和索引:
- 唯一约束:
(owner_id, name)- 同一拥有者的技能名称必须唯一 - 索引:
skills_owner_idx: 拥有者索引skills_created_by_idx: 创建者索引skills_public_idx: 公开状态索引skills_tags_idx: 标签 GIN 索引(支持 JSONB 查询)
SkillFile 模型
技能文件关联表,存储技能关联的文件信息:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
UUID | 主键,自动生成 |
skill_id |
UUID | 技能 ID,外键关联 skills.id |
path |
String(512) | 文件路径,必填 |
file_name |
String(255) | 文件名,必填 |
file_type |
String(50) | 文件类型,必填 |
content |
Text | 文件内容,可选 |
storage_type |
String(20) | 存储类型,默认为 "database" |
storage_key |
String(512) | 存储键(如对象存储的 key),可选 |
size |
Integer | 文件大小(字节),默认为 0 |
created_at |
DateTime | 创建时间 |
updated_at |
DateTime | 更新时间 |
索引:
skill_files_skill_idx: 技能 ID 索引skill_files_path_idx: 技能 ID + 路径复合索引
核心功能
1. 技能列表查询
功能描述: 获取技能列表,支持按用户、公开状态、标签过滤。
权限控制:
- 未登录用户:只能查看公开技能
- 已登录用户:可查看自己的技能 + 公开技能
查询参数:
include_public(bool): 是否包含公开技能,默认 Truetags(List[str]): 标签过滤,支持多标签筛选
实现位置:
- API:
GET /v1/skills - Service:
SkillService.list_skills() - Repository:
SkillRepository.list_by_user()
查询逻辑:
- 如果
user_id存在且include_public=True:返回owner_id == user_id或is_public == True或owner_id == None(系统级公共技能) - 如果
user_id存在且include_public=False:只返回owner_id == user_id的技能 - 如果
user_id不存在且include_public=True:返回所有公开技能 - 如果
user_id不存在且include_public=False:不返回任何结果 - 如果指定了
tags,使用 JSONB 的contains操作符进行标签过滤
2. 技能详情查询
功能描述: 根据技能 ID 获取技能详情,包含关联的文件列表。
权限控制:
- 拥有者:可以访问自己的技能
- 其他用户:只能访问公开的技能
- 未登录用户:只能访问公开的技能
实现位置:
- API:
GET /v1/skills/{skill_id} - Service:
SkillService.get_skill() - Repository:
SkillRepository.get_with_files()
权限检查逻辑:
if skill.owner_id and skill.owner_id != current_user_id and not skill.is_public:
raise ForbiddenException("You don't have permission to access this skill")3. 创建技能
功能描述: 创建新的技能,支持同时创建关联的文件。
权限要求: 需要登录
实现位置:
- API:
POST /v1/skills - Service:
SkillService.create_skill()
业务规则:
- 如果未指定
owner_id,则使用created_by_id作为拥有者 - 检查同一拥有者下是否存在同名技能,如果存在则抛出异常
- 支持在创建时同时添加文件列表
- 文件通过
files参数传入,每个文件包含:path: 文件路径file_name: 文件名file_type: 文件类型content: 文件内容(可选)storage_type: 存储类型(默认 "database")storage_key: 存储键(可选)size: 文件大小(默认 0)
请求体示例:
{
"name": "Python 数据分析",
"description": "使用 Python 进行数据分析的技能",
"content": "详细的技能内容...",
"tags": ["python", "data-analysis"],
"source_type": "local",
"is_public": false,
"files": [
{
"path": "/examples",
"file_name": "example.py",
"file_type": "python",
"content": "print('Hello World')",
"storage_type": "database",
"size": 20
}
]
}4. 更新技能
功能描述: 更新已存在的技能信息。
权限要求: 只有拥有者可以更新自己的技能
实现位置:
- API:
PUT /v1/skills/{skill_id} - Service:
SkillService.update_skill()
业务规则:
- 权限检查:只有拥有者可以更新
- 如果更新名称,需要检查新名称在同一拥有者下是否已存在
- 支持部分更新,所有字段都是可选的
- 更新后自动刷新技能信息
可更新字段:
name: 技能名称description: 技能描述content: 技能内容tags: 标签列表source_type: 来源类型source_url: 来源 URLroot_path: 根路径owner_id: 拥有者 IDis_public: 是否公开license: 许可证
5. 删除技能
功能描述: 删除技能及其关联的所有文件。
权限要求: 只有拥有者可以删除自己的技能
实现位置:
- API:
DELETE /v1/skills/{skill_id} - Service:
SkillService.delete_skill()
业务规则:
- 权限检查:只有拥有者可以删除
- 级联删除:删除技能时自动删除所有关联的文件(通过外键约束)
- 使用
SkillFileRepository.delete_by_skill()显式删除文件记录
6. 添加文件
功能描述: 向已存在的技能添加文件。
权限要求: 只有拥有者可以向自己的技能添加文件
实现位置:
- API:
POST /v1/skills/{skill_id}/files - Service:
SkillService.add_file()
业务规则:
- 权限检查:只有拥有者可以添加文件
- 文件信息包括路径、文件名、类型、内容等
- 支持多种存储类型(默认 "database")
7. 删除文件
功能描述: 删除技能关联的特定文件。
权限要求: 只有拥有者可以删除自己技能的文件
实现位置:
- API:
DELETE /v1/skills/files/{file_id} - Service:
SkillService.delete_file()
业务规则:
- 权限检查:通过文件的
skill_id找到对应的技能,检查拥有者权限 - 删除指定的文件记录
权限控制
访问权限
| 操作 | 拥有者 | 其他用户 | 未登录用户 |
|---|---|---|---|
| 查看自己的技能 | ✅ | ❌ | ❌ |
| 查看公开技能 | ✅ | ✅ | ✅ |
| 创建技能 | ✅ | ❌ | ❌ |
| 更新自己的技能 | ✅ | ❌ | ❌ |
| 删除自己的技能 | ✅ | ❌ | ❌ |
| 添加文件 | ✅ | ❌ | ❌ |
| 删除文件 | ✅ | ❌ | ❌ |
权限检查实现
所有权限检查都在 SkillService 层实现:
- 查看权限: 在
get_skill()方法中检查 - 更新权限: 在
update_skill()方法中检查 - 删除权限: 在
delete_skill()方法中检查 - 文件操作权限: 在
add_file()和delete_file()方法中检查
数据访问层
SkillRepository
提供技能数据访问方法:
list_by_user(): 根据用户 ID、公开状态、标签查询技能列表get_with_files(): 获取技能及其关联的文件(使用selectinload预加载)count_by_user(): 统计用户拥有的技能数量get_by_name_and_owner(): 根据名称和拥有者查询技能(用于唯一性检查)
SkillFileRepository
提供技能文件数据访问方法:
list_by_skill(): 获取技能的所有文件delete_by_skill(): 删除技能的所有文件(批量删除)
API 端点
基础路径
所有技能相关的 API 端点都在 /v1/skills 路径下。
端点列表
| 方法 | 路径 | 说明 | 需要认证 |
|---|---|---|---|
| GET | /v1/skills |
获取技能列表 | 可选 |
| POST | /v1/skills |
创建技能 | ✅ |
| GET | /v1/skills/{skill_id} |
获取技能详情 | 可选 |
| PUT | /v1/skills/{skill_id} |
更新技能 | ✅ |
| DELETE | /v1/skills/{skill_id} |
删除技能 | ✅ |
| POST | /v1/skills/{skill_id}/files |
添加文件 | ✅ |
| DELETE | /v1/skills/files/{file_id} |
删除文件 | ✅ |
响应格式
所有 API 响应都遵循统一的格式:
成功响应:
{
"success": true,
"data": { ... }
}错误响应:
{
"success": false,
"error": "错误信息"
}异常处理
技能模块使用以下自定义异常:
NotFoundException: 资源不存在(如技能或文件不存在)ForbiddenException: 权限不足(如非拥有者尝试修改技能)BadRequestException: 请求参数错误(如同名技能已存在)
所有异常都在 SkillService 层抛出,由 API 层的全局异常处理器统一处理。
数据库关系
外键关系
Skill → AuthUser (owner)
owner_id→user.idondelete="SET NULL": 用户删除时,拥有者设为 NULL
Skill → AuthUser (created_by)
created_by_id→user.idondelete="CASCADE": 用户删除时,删除其创建的所有技能
SkillFile → Skill
skill_id→skills.idondelete="CASCADE": 技能删除时,级联删除所有关联文件
关系加载策略
Skill.owner:lazy="selectin"- 使用 selectin 加载Skill.created_by:lazy="selectin"- 使用 selectin 加载Skill.files:lazy="selectin"- 使用 selectin 加载,支持级联删除SkillFile.skill:lazy="selectin"- 使用 selectin 加载
使用示例
创建技能
from app.services.skill_service import SkillService
service = SkillService(db)
skill = await service.create_skill(
created_by_id="user123",
name="Python 爬虫",
description="使用 Python 进行网页爬取的技能",
content="详细的技能内容...",
tags=["python", "web-scraping"],
is_public=True,
files=[
{
"path": "/examples",
"file_name": "scraper.py",
"file_type": "python",
"content": "import requests\n...",
"storage_type": "database",
"size": 1024
}
]
)查询技能列表
# 获取当前用户的所有技能(包括公开的)
skills = await service.list_skills(
current_user_id="user123",
include_public=True,
tags=["python"]
)更新技能
skill = await service.update_skill(
skill_id=skill.id,
current_user_id="user123",
description="更新后的描述",
is_public=False
)扩展性考虑
未来可能的扩展
- 版本管理: 支持技能版本控制,记录历史版本
- 评分系统: 允许用户对公开技能进行评分和评论
- 使用统计: 记录技能的使用次数和频率
- 导入导出: 支持技能的批量导入和导出
- 模板系统: 支持从模板创建技能
- 协作功能: 支持多人协作编辑技能
- 搜索优化: 使用全文搜索提升搜索性能
- 文件存储优化: 支持大文件使用对象存储(S3、OSS 等)
注意事项
- 唯一性约束: 同一拥有者的技能名称必须唯一,创建和更新时都会检查
- 权限边界: 所有权限检查都在服务层实现,确保数据安全
- 级联删除: 删除技能时会自动删除所有关联文件,注意数据备份
- 标签查询: 使用 PostgreSQL 的 JSONB GIN 索引,支持高效的标签查询
- 关系加载: 使用
selectinload策略,避免 N+1 查询问题 - 事务管理: 所有写操作都在事务中执行,确保数据一致性
相关文件
- 模型定义:
app/models/skill.py - 数据访问:
app/repositories/skill.py - 业务逻辑:
app/services/skill_service.py - API 端点:
app/api/v1/skills.py - 数据库迁移:
alembic/versions/