Files
ttstd-infinite-canvas/backend/internal/repository/asset_library.go
T
李祥生 c0bf81c590 fix(assets): 统一素材库 entity 口径并修复远端空结果遮蔽本地素材
- 个人素材库列表与 facets 统一排除 entity 类型,计数与列表同口径,修复“总数 30 但列表为空”
- 个人素材页 /assets、参考内容弹窗、项目资产页三处“远端空数组 truthy 遮蔽本地素材”统一修复:远端无可展示素材时回退本地列表,分页、分类、关键词过滤同步切换口径
- 项目资产页待确认候选不再硬编码 character 分类:分类切换联动查询,角色/道具/场景动态图标与文案,仅角色类展示“归并到角色”
- 项目资产页分类计数改为独立查询并与 tab 解耦,修复切 tab 时总数漂移;确认资产后同步失效候选与资产列表缓存;切分类重置候选页码
2026-09-15 17:28:28 +08:00

215 lines
7.0 KiB
Go

package repository
import (
"encoding/json"
"errors"
"strings"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
)
type UserAssetPageFilter struct {
Kind string
Category string
FolderID *string
Uncategorized bool
Status string
Query string
}
type UserAssetFacetRow struct {
Key string
Count int64
}
func (r *Repository) UserAssetsPage(userID string, page int, pageSize int, filter UserAssetPageFilter) ([]model.Asset, int64, error) {
var assets []model.Asset
var total int64
// 素材库页面只展示媒体与文本素材;entity 角色卡由项目资产页管理。列表与 facets 必须同口径排除,
// 否则 facets 会计入 entity,前端出现“全部计数 30 但列表为空”的矛盾。
query := userAssetFilteredQuery(r.db.Model(&model.Asset{}).Where("user_id = ? AND kind <> ?", userID, "entity"), filter, true)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Order("updated_at desc, id desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&assets).Error
return assets, total, err
}
func (r *Repository) UserAssetFacets(userID string, status string) ([]UserAssetFacetRow, []UserAssetFacetRow, []UserAssetFacetRow, error) {
base := func() *gorm.DB {
return userAssetFilteredQuery(r.db.Model(&model.Asset{}).Where("user_id = ? AND kind <> ?", userID, "entity"), UserAssetPageFilter{Status: status}, false)
}
var kindRows []UserAssetFacetRow
if err := base().Select("kind AS key, COUNT(*) AS count").Group("kind").Scan(&kindRows).Error; err != nil {
return nil, nil, nil, err
}
var categoryRows []UserAssetFacetRow
if err := base().Select("category AS key, COUNT(*) AS count").Group("category").Scan(&categoryRows).Error; err != nil {
return nil, nil, nil, err
}
var folderRows []UserAssetFacetRow
if err := base().Select("folder_id AS key, COUNT(*) AS count").Group("folder_id").Scan(&folderRows).Error; err != nil {
return nil, nil, nil, err
}
return kindRows, categoryRows, folderRows, nil
}
func userAssetFilteredQuery(query *gorm.DB, filter UserAssetPageFilter, includeSearch bool) *gorm.DB {
if value := strings.TrimSpace(filter.Kind); value != "" {
query = query.Where("kind = ?", value)
}
if value := strings.TrimSpace(filter.Category); value != "" {
query = query.Where("category = ?", value)
}
if filter.Uncategorized {
query = query.Where("folder_id = ''")
} else if filter.FolderID != nil {
query = query.Where("folder_id = ?", strings.TrimSpace(*filter.FolderID))
}
switch strings.TrimSpace(filter.Status) {
case "active":
query = query.Where("status <> ?", model.AssetVersionStatusArchived)
case "archived":
query = query.Where("status = ?", model.AssetVersionStatusArchived)
case "":
default:
query = query.Where("status = ?", strings.TrimSpace(filter.Status))
}
if includeSearch {
if value := strings.ToLower(strings.TrimSpace(filter.Query)); value != "" {
pattern := "%" + value + "%"
query = query.Where("LOWER(title) LIKE ? OR LOWER(payload_json) LIKE ?", pattern, pattern)
}
}
return query
}
func (r *Repository) AssetFolders(userID string) ([]model.AssetFolder, error) {
var folders []model.AssetFolder
err := r.db.Where("user_id = ?", userID).Order("position asc, created_at asc").Find(&folders).Error
return folders, err
}
func (r *Repository) AssetFolderForUser(userID string, folderID string) (*model.AssetFolder, error) {
var folder model.AssetFolder
if err := r.db.First(&folder, "id = ? AND user_id = ?", folderID, userID).Error; err != nil {
return nil, err
}
return &folder, nil
}
func (r *Repository) AssetFolderNameExists(userID string, nameKey string, excludeID string) (bool, error) {
query := r.db.Model(&model.AssetFolder{}).Where("user_id = ? AND name_key = ?", userID, nameKey)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
var count int64
err := query.Count(&count).Error
return count > 0, err
}
func (r *Repository) NextAssetFolderPosition(userID string) (int, error) {
var row struct{ Maximum int }
err := r.db.Model(&model.AssetFolder{}).Select("COALESCE(MAX(position), -1) AS maximum").Where("user_id = ?", userID).Scan(&row).Error
return row.Maximum + 1, err
}
func (r *Repository) CreateAssetFolder(folder *model.AssetFolder) error {
return r.db.Create(folder).Error
}
func (r *Repository) UpdateAssetFolder(folder *model.AssetFolder) error {
result := r.db.Model(&model.AssetFolder{}).Where("id = ? AND user_id = ?", folder.ID, folder.UserID).Updates(map[string]any{
"name": folder.Name, "name_key": folder.NameKey, "updated_at": folder.UpdatedAt,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *Repository) MoveUserAssetsToFolder(userID string, assetIDs []string, folderID string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
return moveUserAssetsToFolder(tx, userID, assetIDs, folderID)
})
}
func (r *Repository) DeleteAssetFolder(userID string, folderID string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var folder model.AssetFolder
if err := tx.First(&folder, "id = ? AND user_id = ?", folderID, userID).Error; err != nil {
return err
}
var assets []model.Asset
if err := tx.Where("user_id = ? AND folder_id = ?", userID, folderID).Find(&assets).Error; err != nil {
return err
}
ids := make([]string, len(assets))
for index := range assets {
ids[index] = assets[index].ID
}
if err := moveUserAssetsToFolder(tx, userID, ids, ""); err != nil {
return err
}
result := tx.Delete(&model.AssetFolder{}, "id = ? AND user_id = ?", folderID, userID)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrRecordNotFound
}
return nil
})
}
func moveUserAssetsToFolder(tx *gorm.DB, userID string, assetIDs []string, folderID string) error {
if len(assetIDs) == 0 {
return nil
}
var assets []model.Asset
if err := tx.Where("user_id = ? AND id IN ?", userID, assetIDs).Find(&assets).Error; err != nil {
return err
}
if len(assets) != len(assetIDs) {
return gorm.ErrRecordNotFound
}
now := time.Now().UTC()
for index := range assets {
payloadJSON, err := assetPayloadWithFolder(assets[index].PayloadJSON, folderID, now)
if err != nil {
return err
}
if err := tx.Model(&model.Asset{}).Where("id = ? AND user_id = ?", assets[index].ID, userID).Updates(map[string]any{
"folder_id": folderID, "payload_json": payloadJSON, "updated_at": now,
}).Error; err != nil {
return err
}
}
return nil
}
func assetPayloadWithFolder(payloadJSON string, folderID string, updatedAt time.Time) (string, error) {
var payload map[string]any
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
return "", err
}
if folderID == "" {
delete(payload, "folderId")
} else {
payload["folderId"] = folderID
}
payload["updatedAt"] = updatedAt.Format(time.RFC3339Nano)
encoded, err := json.Marshal(payload)
return string(encoded), err
}
func IsAssetFolderNotFound(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound)
}