feat(skills): 技能管理 - 增加可选的平台子分类策展 (#693)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5976389ed4
commit
c963fc7b89
32 files changed
+981
-24
No files matched your search
@@ -21,6 +21,13 @@ func (r *Repository) BuiltinSkillTombstoned(skillID string) (bool, error) {
|
||||
func (r *Repository) DeleteBuiltinSkill(skillID string, actorID string) error {
|
||||
now := time.Now().UTC()
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var locked model.Skill
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&locked, "id = ?", skillID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&model.SkillCurationAssignment{}, "skill_id = ?", skillID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "skill_id"}}, DoUpdates: clause.AssignmentColumns([]string{"deleted_by", "deleted_at"})}).Create(&model.BuiltinSkillTombstone{SkillID: skillID, DeletedBy: actorID, DeletedAt: now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"infinite-canvas/backend/internal/kernel"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
type SkillCuration struct {
|
||||
model.SkillCurationSetting
|
||||
Categories []model.SkillCurationCategory `json:"categories"`
|
||||
Assignments []model.SkillCurationAssignment `json:"assignments"`
|
||||
}
|
||||
|
||||
// Read the version before and after the snapshot so concurrent edits cannot
|
||||
// produce a mixed snapshot that a client could save with a newer revision.
|
||||
func (r *Repository) SkillCuration(admin bool) (*SkillCuration, error) {
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
out := &SkillCuration{Categories: []model.SkillCurationCategory{}, Assignments: []model.SkillCurationAssignment{}}
|
||||
if err := r.db.First(&out.SkillCurationSetting, 1).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !admin && !out.Enabled {
|
||||
return out, nil
|
||||
}
|
||||
query := r.db.Order("sort_order, id")
|
||||
if !admin {
|
||||
query = query.Where("enabled = ?", true)
|
||||
}
|
||||
if err := query.Find(&out.Categories).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = r.db.Model(&model.SkillCurationAssignment{}).Select("skill_curation_assignments.*").
|
||||
Joins("JOIN skills ON skills.id = skill_curation_assignments.skill_id").Where("skills.status = 1 AND skills.is_private = false")
|
||||
if !admin {
|
||||
query = query.Joins("JOIN skill_curation_categories c ON c.id = skill_curation_assignments.category_id").Where("c.enabled = true AND c.root_tag = skills.tag")
|
||||
}
|
||||
if err := query.Order("skill_id, category_id").Find(&out.Assignments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var after model.SkillCurationSetting
|
||||
if err := r.db.First(&after, 1).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if after.Revision == out.Revision {
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
return nil, kernel.NewAppError(409, "分类正在更新,请重新加载")
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateSkillCuration(actorID string, revision int64, requestID string, change func(*Repository) error) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.SkillCurationSetting{}).Where("id = 1 AND revision = ?", revision).Update("revision", revision+1)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return kernel.NewAppError(409, "分类版本已更新,请重新加载")
|
||||
}
|
||||
var actor model.User
|
||||
if err := tx.First(&actor, "id = ?", actorID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if actor.Role != model.UserRoleAdmin || actor.Status != model.UserStatusActive {
|
||||
return kernel.Forbidden("需要管理员权限")
|
||||
}
|
||||
repo := New(tx)
|
||||
before, err := repo.SkillCuration(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := change(repo); err != nil {
|
||||
return err
|
||||
}
|
||||
after, err := repo.SkillCuration(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before.Revision = revision
|
||||
metadata, err := json.Marshal(map[string]any{"requestId": requestID, "before": before, "after": after})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&model.AdminAuditEvent{ID: kernel.NewID(), ActorUserID: actorID, Action: "skill_curation.update", TargetType: "skill_curation", TargetID: "1", MetadataJSON: string(metadata)}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) SetSkillCurationEnabled(enabled bool) error {
|
||||
return r.db.Model(&model.SkillCurationSetting{}).Where("id = 1").Update("enabled", enabled).Error
|
||||
}
|
||||
func (r *Repository) SaveSkillCurationCategory(category *model.SkillCurationCategory) error {
|
||||
return r.db.Save(category).Error
|
||||
}
|
||||
func (r *Repository) CurationSkill(id string) (*model.Skill, error) {
|
||||
var row model.Skill
|
||||
err := r.db.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, "id = ? AND status = 1 AND is_private = false", id).Error
|
||||
return &row, err
|
||||
}
|
||||
func (r *Repository) AssignSkillCuration(skillID string, ids []string) error {
|
||||
if err := r.db.Delete(&model.SkillCurationAssignment{}, "skill_id = ?", skillID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range ids {
|
||||
if err := r.db.Create(&model.SkillCurationAssignment{SkillID: skillID, CategoryID: id}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const effectiveCurationCategory = `SELECT 1 FROM skill_curation_assignments a JOIN skill_curation_categories c ON c.id = a.category_id WHERE a.skill_id = skills.id AND c.enabled = true AND c.root_tag = skills.tag AND skills.is_private = false`
|
||||
@@ -11,15 +11,17 @@ import (
|
||||
)
|
||||
|
||||
type SkillListFilter struct {
|
||||
UserID string
|
||||
Scope string
|
||||
Search string
|
||||
Tag string
|
||||
LibraryCategoryID string
|
||||
LibraryUncategorized bool
|
||||
Sort string
|
||||
Limit int
|
||||
Offset int
|
||||
PlatformCategoryID string
|
||||
PlatformUncategorized bool
|
||||
UserID string
|
||||
Scope string
|
||||
Search string
|
||||
Tag string
|
||||
LibraryCategoryID string
|
||||
LibraryUncategorized bool
|
||||
Sort string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type SkillMetrics struct {
|
||||
@@ -77,6 +79,12 @@ func (r *Repository) Skills(filter SkillListFilter) ([]model.Skill, int64, error
|
||||
if filter.Tag != "" {
|
||||
query = query.Where("skills.tag = ?", filter.Tag)
|
||||
}
|
||||
if filter.PlatformCategoryID != "" {
|
||||
query = query.Where("EXISTS ("+effectiveCurationCategory+" AND c.id = ?)", filter.PlatformCategoryID)
|
||||
}
|
||||
if filter.PlatformUncategorized {
|
||||
query = query.Where("NOT EXISTS (" + effectiveCurationCategory + ")")
|
||||
}
|
||||
if filter.LibraryCategoryID != "" {
|
||||
query = query.Where("user_skill_states.library_category_id = ?", filter.LibraryCategoryID)
|
||||
} else if filter.LibraryUncategorized {
|
||||
@@ -132,6 +140,13 @@ func (r *Repository) CreateSkill(skill *model.Skill, ownerState *model.UserSkill
|
||||
|
||||
func (r *Repository) DeleteSkill(id string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var locked model.Skill
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&locked, "id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&model.SkillCurationAssignment{}, "skill_id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&model.UserSkillState{}, "skill_id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user