Files
ttstd-infinite-canvas/backend/internal/repository/tools.go

201 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repository
import (
"errors"
"strings"
"time"
"yingce/backend/internal/kernel"
"yingce/backend/internal/model"
"yingce/backend/internal/tools"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// UpsertBuiltinTools 按 id 幂等更新内置工具;created_at 保留首次写入值。
func (r *Repository) UpsertBuiltinTools(tools []model.Tool) error {
if len(tools) == 0 {
return nil
}
return r.db.Transaction(func(tx *gorm.DB) error {
ids := make([]int64, 0, len(tools))
for _, tool := range tools {
ids = append(ids, tool.ID)
}
var collisions int64
if err := tx.Model(&model.Tool{}).Where("id IN ? AND source <> ?", ids, "builtin").Count(&collisions).Error; err != nil {
return err
}
if collisions > 0 {
return errors.New("内置工具 ID 与用户工具冲突,拒绝覆盖")
}
result := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: clause.Column{Table: "tools", Name: "source"}, Value: "builtin"}}},
DoUpdates: clause.AssignmentColumns([]string{
"type", "label_en", "label", "desc", "tag", "cover", "extra_info_json", "prompt", "ratio",
"media_url", "owner_id", "source", "enabled", "visibility", "sort_weight", "updated_at",
}),
}).Create(&tools)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != int64(len(tools)) {
return errors.New("内置工具写入遇到 ID 冲突,已回滚")
}
// 内置工具显式写入自增主键,PostgreSQL 的序列不会自动前移;这里同步到当前最大值,
// 避免用户自建工具从 1 重新分配而与内置工具主键冲突。
if r.Dialect() != "postgres" {
return nil
}
return tx.Exec("SELECT setval(pg_get_serial_sequence('tools', 'id'), (SELECT COALESCE(MAX(id), 1) FROM tools), true)").Error
})
}
// ListTools 按范围查询工具列表并携带当前用户收藏状态。
func (r *Repository) ListTools(userID string, req tools.ToolListRequest) ([]tools.ToolWithFavorite, int64, error) {
base := r.db.Table("tools").Where("enabled = ? AND (tools.visibility = ? OR tools.owner_id = ?)", true, tools.ToolVisibilityPublic, userID)
switch req.Scope {
case tools.ToolScopeFavorites:
base = base.Joins("JOIN tool_favorites tf ON tf.tool_id = tools.id AND tf.user_id = ?", userID)
default: // public / custom
base = base.Joins("LEFT JOIN tool_favorites tf ON tf.tool_id = tools.id AND tf.user_id = ?", userID)
}
// 可见性:公共 scope 只看 public(含他人公开),其余 scope 只看自己可见的
switch req.Scope {
case tools.ToolScopePublic:
base = base.Where("tools.visibility = ?", tools.ToolVisibilityPublic)
case tools.ToolScopeFavorites:
base = base.Where("tf.user_id = ?", userID)
case tools.ToolScopeCustom:
base = base.Where("tools.owner_id = ? AND tools.source = ?", userID, tools.ToolSourceUser)
}
if req.Type != "" {
base = base.Where("tools.type = ?", req.Type)
}
if req.Tag != "" {
base = base.Where("tools.tag = ?", req.Tag)
}
if req.Search != "" {
keyword := "%" + strings.ToLower(req.Search) + "%"
base = base.Where("(LOWER(tools.label) LIKE ? OR LOWER(tools.label_en) LIKE ? OR LOWER(tools.desc) LIKE ?)", keyword, keyword, keyword)
}
var total int64
if err := base.Session(&gorm.Session{}).Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "tools.sort_weight ASC, tools.id ASC"
switch req.Scope {
case tools.ToolScopeFavorites:
orderBy = "tf.created_at DESC, tools.id DESC"
}
offset := (req.Page - 1) * req.PageSize
var rows []struct {
model.Tool
TFID *int64 `gorm:"column:tf_id"`
TFCreatedAt *time.Time `gorm:"column:tf_created_at"`
}
// 列表只取摘要字段,不取 extra_info_json、prompt 两个大字段;列与 tools.ToolSummary 对齐。
selectColumns := "tools.id, tools.type, tools.label_en, tools.label, tools.desc, tools.tag, tools.cover, " +
"tools.ratio, tools.media_url, tools.owner_id, tools.source, tools.enabled, tools.visibility, " +
"tools.sort_weight, tools.created_at, tools.updated_at, tf.id AS tf_id, tf.created_at AS tf_created_at"
err := base.Session(&gorm.Session{}).
Select(selectColumns).
Order(orderBy).
Offset(offset).
Limit(req.PageSize).
Scan(&rows).Error
if err != nil {
return nil, 0, err
}
items := make([]tools.ToolWithFavorite, 0, len(rows))
for _, row := range rows {
items = append(items, tools.ToolWithFavorite{
Tool: row.Tool,
Favorited: row.TFID != nil,
FavoritedAt: row.TFCreatedAt,
})
}
return items, total, nil
}
// ToolForUser 校验可见性后返回工具。
func (r *Repository) ToolForUser(userID string, toolID int64) (model.Tool, error) {
var tool model.Tool
err := r.db.Where("id = ? AND enabled = ? AND (visibility = ? OR owner_id = ?)", toolID, true, tools.ToolVisibilityPublic, userID).
First(&tool).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return tool, kernel.NotFound("工具不存在或不可见")
}
if err != nil {
return tool, err
}
return tool, nil
}
// ToolFavorited 返回工具及收藏时间(未收藏时为 nil)。
func (r *Repository) ToolFavorited(userID string, toolID int64) (model.Tool, *time.Time, error) {
tool, err := r.ToolForUser(userID, toolID)
if err != nil {
return tool, nil, err
}
var favorite model.ToolFavorite
err = r.db.Where("user_id = ? AND tool_id = ?", userID, toolID).First(&favorite).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return tool, nil, nil
}
if err != nil {
return tool, nil, err
}
return tool, &favorite.CreatedAt, nil
}
// AddToolFavorite 幂等添加收藏;已收藏时直接成功。
func (r *Repository) AddToolFavorite(userID string, toolID int64) error {
favorite := model.ToolFavorite{UserID: userID, ToolID: toolID, CreatedAt: time.Now()}
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "tool_id"}},
DoNothing: true,
}).Create(&favorite).Error
}
// RemoveToolFavorite 取消收藏;未收藏时也无错误。
func (r *Repository) RemoveToolFavorite(userID string, toolID int64) error {
return r.db.Where("user_id = ? AND tool_id = ?", userID, toolID).
Delete(&model.ToolFavorite{}).Error
}
// CreateTool 创建用户自定义工具。
func (r *Repository) CreateTool(tool *model.Tool) (*model.Tool, error) {
if err := r.db.Create(tool).Error; err != nil {
return nil, err
}
return tool, nil
}
// DeleteUserTool 仅允许删除自己的自定义工具;事务内同步清理收藏记录。
func (r *Repository) DeleteUserTool(userID string, toolID int64) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var tool model.Tool
err := tx.Where("id = ? AND owner_id = ? AND source = ?", toolID, userID, tools.ToolSourceUser).
First(&tool).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return kernel.NotFound("工具不存在或不支持删除")
}
if err != nil {
return err
}
if err := tx.Where("tool_id = ?", toolID).Delete(&model.ToolFavorite{}).Error; err != nil {
return err
}
return tx.Delete(&model.Tool{}, toolID).Error
})
}