Files
ttstd-infinite-canvas/backend/internal/app/asset_library.go

204 lines
6.0 KiB
Go

package app
import (
"encoding/json"
"errors"
"strings"
"time"
"unicode/utf8"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"gorm.io/gorm"
)
type UserAssetPage struct {
Assets []json.RawMessage `json:"assets"`
KindCounts map[string]int64 `json:"kindCounts"`
CategoryCounts map[string]int64 `json:"categoryCounts"`
FolderCounts map[string]int64 `json:"folderCounts"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
Total int64 `json:"total"`
HasMore bool `json:"hasMore"`
}
type UserAssetPageFilter struct {
Kind string
Category string
FolderID *string
Uncategorized bool
Status string
Query string
}
type CreateAssetFolderRequest struct {
Name string `json:"name"`
}
type UpdateAssetFolderRequest struct {
Name string `json:"name"`
}
type MoveUserAssetsRequest struct {
AssetIDs []string `json:"assetIds"`
FolderID string `json:"folderId"`
}
func (s *Service) UserAssetsPage(userID string, page int, pageSize int, filter UserAssetPageFilter) (UserAssetPage, error) {
page, pageSize = normalizeProjectPage(page, pageSize, 120)
repoFilter := repository.UserAssetPageFilter{
Kind: filter.Kind, Category: filter.Category, FolderID: filter.FolderID,
Uncategorized: filter.Uncategorized, Status: filter.Status, Query: filter.Query,
}
assets, total, err := s.repo.UserAssetsPage(userID, page, pageSize, repoFilter)
if err != nil {
return UserAssetPage{}, err
}
rawAssets := make([]json.RawMessage, 0, len(assets))
for _, asset := range assets {
if payload := clientAssetPayload(asset); len(payload) > 0 {
rawAssets = append(rawAssets, payload)
}
}
kindRows, categoryRows, folderRows, err := s.repo.UserAssetFacets(userID, filter.Status)
if err != nil {
return UserAssetPage{}, err
}
return UserAssetPage{
Assets: rawAssets, KindCounts: assetFacetMap(kindRows), CategoryCounts: assetFacetMap(categoryRows), FolderCounts: assetFacetMap(folderRows),
Page: page, PageSize: pageSize, Total: total, HasMore: int64(page*pageSize) < total,
}, nil
}
func assetFacetMap(rows []repository.UserAssetFacetRow) map[string]int64 {
result := make(map[string]int64, len(rows))
for _, row := range rows {
result[row.Key] = row.Count
}
return result
}
func (s *Service) AssetFolders(userID string) ([]model.AssetFolder, error) {
return s.repo.AssetFolders(userID)
}
func (s *Service) CreateAssetFolder(userID string, req CreateAssetFolderRequest) (model.AssetFolder, error) {
name, nameKey, err := normalizeAssetFolderName(req.Name)
if err != nil {
return model.AssetFolder{}, err
}
exists, err := s.repo.AssetFolderNameExists(userID, nameKey, "")
if err != nil {
return model.AssetFolder{}, err
}
if exists {
return model.AssetFolder{}, BadAuthRequest("已存在同名素材分类")
}
position, err := s.repo.NextAssetFolderPosition(userID)
if err != nil {
return model.AssetFolder{}, err
}
now := time.Now().UTC()
folder := model.AssetFolder{ID: newID(), UserID: userID, Name: name, NameKey: nameKey, Position: position, CreatedAt: now, UpdatedAt: now}
if err := s.repo.CreateAssetFolder(&folder); err != nil {
return model.AssetFolder{}, err
}
return folder, nil
}
func (s *Service) UpdateAssetFolder(userID string, folderID string, req UpdateAssetFolderRequest) (model.AssetFolder, error) {
folder, err := s.repo.AssetFolderForUser(userID, strings.TrimSpace(folderID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return model.AssetFolder{}, BadAuthRequest("素材分类不存在")
}
return model.AssetFolder{}, err
}
name, nameKey, err := normalizeAssetFolderName(req.Name)
if err != nil {
return model.AssetFolder{}, err
}
exists, err := s.repo.AssetFolderNameExists(userID, nameKey, folder.ID)
if err != nil {
return model.AssetFolder{}, err
}
if exists {
return model.AssetFolder{}, BadAuthRequest("已存在同名素材分类")
}
folder.Name = name
folder.NameKey = nameKey
folder.UpdatedAt = time.Now().UTC()
if err := s.repo.UpdateAssetFolder(folder); err != nil {
return model.AssetFolder{}, err
}
return *folder, nil
}
func (s *Service) DeleteAssetFolder(userID string, folderID string) error {
s.storageMu.Lock()
defer s.storageMu.Unlock()
err := s.repo.DeleteAssetFolder(userID, strings.TrimSpace(folderID))
if errors.Is(err, gorm.ErrRecordNotFound) {
return BadAuthRequest("素材分类不存在")
}
return err
}
func (s *Service) MoveUserAssetsToFolder(userID string, req MoveUserAssetsRequest) error {
ids := uniqueNonemptyStrings(req.AssetIDs)
if len(ids) == 0 {
return BadAuthRequest("请选择要移动的素材")
}
if len(ids) > 200 {
return BadAuthRequest("一次最多移动 200 个素材")
}
s.storageMu.Lock()
defer s.storageMu.Unlock()
folderID := strings.TrimSpace(req.FolderID)
if folderID != "" {
if _, err := s.repo.AssetFolderForUser(userID, folderID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return BadAuthRequest("目标素材分类不存在")
}
return err
}
}
if err := s.repo.MoveUserAssetsToFolder(userID, ids, folderID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return BadAuthRequest("部分素材不存在或不属于当前用户")
}
return err
}
return nil
}
func normalizeAssetFolderName(value string) (string, string, error) {
name := strings.TrimSpace(value)
if name == "" {
return "", "", BadAuthRequest("请输入素材分类名称")
}
if utf8.RuneCountInString(name) > 40 {
return "", "", BadAuthRequest("素材分类名称不能超过 40 个字符")
}
return name, strings.ToLower(name), nil
}
func uniqueNonemptyStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}