feat(assets): 素材库与画布 - 新增可交互通用文件夹

This commit is contained in:
echoD886 committed 2026-08-20 15:16:45 +08:00
1 parent 9ee71d4650
commit 935108edca
35 files changed
+2010 -155

No files matched your search

+6 -1
View File
@@ -73,6 +73,7 @@ type ProjectDetail struct {
Canvases []model.CanvasProject `json:"canvases"`
CanvasUnitLinks []model.CanvasUnitLink `json:"canvasUnitLinks"`
Assets []ProjectAssetSummary `json:"assets"`
AssetFolders []model.ProjectAssetFolder `json:"assetFolders"`
Workflows []ProjectWorkflowDetail `json:"workflows"`
Shots []model.Shot `json:"shots"`
ShotReferences []model.ShotAssetReference `json:"shotReferences"`
@@ -138,6 +139,10 @@ func (s *Service) ProjectDetail(userID string, id string) (ProjectDetail, error)
if err != nil {
return ProjectDetail{}, err
}
assetFolders, err := s.ProjectAssetFolders(userID, project.ID)
if err != nil {
return ProjectDetail{}, err
}
workflows, err := s.ProjectWorkflows(project.ID)
if err != nil {
return ProjectDetail{}, err
@@ -154,7 +159,7 @@ func (s *Service) ProjectDetail(userID string, id string) (ProjectDetail, error)
if err != nil {
return ProjectDetail{}, err
}
return ProjectDetail{Project: *project, Units: units, Canvases: canvases, CanvasUnitLinks: canvasUnitLinks, Assets: assets, Workflows: workflows, Shots: shots, ShotReferences: shotReferences, AssetCandidates: candidates}, nil
return ProjectDetail{Project: *project, Units: units, Canvases: canvases, CanvasUnitLinks: canvasUnitLinks, Assets: assets, AssetFolders: assetFolders, Workflows: workflows, Shots: shots, ShotReferences: shotReferences, AssetCandidates: candidates}, nil
}
func (s *Service) CreateProject(userID string, req CreateProjectRequest) (model.Project, error) {
+85 -46
View File
@@ -9,12 +9,14 @@ import (
)
type LinkProjectAssetRequest struct {
AssetID string `json:"assetId"`
Category string `json:"category"`
AssetID string `json:"assetId"`
Category string `json:"category"`
FolderID *string `json:"folderId"`
}
type UpdateProjectAssetCategoryRequest struct {
Category string `json:"category"`
type UpdateProjectAssetRequest struct {
Category *string `json:"category"`
FolderID *string `json:"folderId"`
}
type CreateAssetVersionRequest struct {
@@ -32,6 +34,10 @@ type ProjectAssetSummary struct {
PrimaryVersionID string `json:"primaryVersionId,omitempty"`
VersionCount int `json:"versionCount"`
Usages []string `json:"usages"`
FolderID string `json:"folderId,omitempty"`
Position int `json:"position"`
StorageKey string `json:"storageKey,omitempty"`
PreviewText string `json:"previewText,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
Character *CharacterCardSummary `json:"character,omitempty"`
}
@@ -103,20 +109,16 @@ func (s *Service) LinkProjectAsset(userID string, projectID string, req LinkProj
if !validAssetCategory(category) {
return ProjectAssetSummary{}, BadAuthRequest("不支持的资产业务分类")
}
folderID, err := s.resolveProjectAssetFolderID(projectID, req.FolderID)
if err != nil {
return ProjectAssetSummary{}, err
}
linked, err := s.repo.ProjectAssetLinked(projectID, asset.ID)
if err != nil {
return ProjectAssetSummary{}, err
}
if linked {
versions, versionErr := s.repo.AssetVersions(asset.ID)
if versionErr != nil {
return ProjectAssetSummary{}, versionErr
}
usages, usageErr := s.repo.ProjectAssetUsageRoles(projectID, asset.ID)
if usageErr != nil {
return ProjectAssetSummary{}, usageErr
}
return ProjectAssetSummary{ID: asset.ID, Title: asset.Title, MediaType: asset.Kind, Category: asset.Category, Status: asset.Status, PrimaryVersionID: asset.PrimaryVersionID, VersionCount: len(versions), Usages: usages, UpdatedAt: asset.UpdatedAt}, nil
return s.projectAssetSummary(userID, projectID, asset)
}
now := time.Now()
asset.Category = category
@@ -135,7 +137,11 @@ func (s *Service) LinkProjectAsset(userID string, projectID string, req LinkProj
versions = append(versions, version)
}
asset.UpdatedAt = now
link := model.ProjectAssetLink{ID: newID(), ProjectID: projectID, AssetID: asset.ID, CreatedAt: now}
position, err := s.repo.NextProjectAssetPosition(projectID, folderID)
if err != nil {
return ProjectAssetSummary{}, err
}
link := model.ProjectAssetLink{ID: newID(), ProjectID: projectID, AssetID: asset.ID, FolderID: folderID, Position: position, CreatedAt: now}
created, err := s.repo.LinkProjectAsset(asset, initialVersion, &link)
if err != nil {
return ProjectAssetSummary{}, err
@@ -145,17 +151,9 @@ func (s *Service) LinkProjectAsset(userID string, projectID string, req LinkProj
if currentErr != nil {
return ProjectAssetSummary{}, currentErr
}
currentVersions, versionErr := s.repo.AssetVersions(asset.ID)
if versionErr != nil {
return ProjectAssetSummary{}, versionErr
}
usages, usageErr := s.repo.ProjectAssetUsageRoles(projectID, asset.ID)
if usageErr != nil {
return ProjectAssetSummary{}, usageErr
}
return ProjectAssetSummary{ID: current.ID, Title: current.Title, MediaType: current.Kind, Category: current.Category, Status: current.Status, PrimaryVersionID: current.PrimaryVersionID, VersionCount: len(currentVersions), Usages: usages, UpdatedAt: current.UpdatedAt}, nil
return s.projectAssetSummary(userID, projectID, current)
}
return ProjectAssetSummary{ID: asset.ID, Title: asset.Title, MediaType: asset.Kind, Category: asset.Category, Status: asset.Status, PrimaryVersionID: asset.PrimaryVersionID, VersionCount: len(versions), Usages: []string{}, UpdatedAt: asset.UpdatedAt}, nil
return s.projectAssetSummary(userID, projectID, asset)
}
func (s *Service) UnlinkProjectAsset(userID string, projectID string, assetID string) error {
@@ -214,7 +212,7 @@ func canvasReferencesCharacterAsset(payloadJSON string, assetID string) (bool, e
return false, nil
}
func (s *Service) UpdateProjectAssetCategory(userID string, projectID string, assetID string, req UpdateProjectAssetCategoryRequest) (ProjectAssetSummary, error) {
func (s *Service) UpdateProjectAsset(userID string, projectID string, assetID string, req UpdateProjectAssetRequest) (ProjectAssetSummary, error) {
if _, err := s.repo.ProjectForUser(userID, projectID); err != nil {
return ProjectAssetSummary{}, err
}
@@ -229,27 +227,38 @@ func (s *Service) UpdateProjectAssetCategory(userID string, projectID string, as
if !linked {
return ProjectAssetSummary{}, BadAuthRequest("素材尚未加入当前项目")
}
category := model.AssetCategory(strings.TrimSpace(req.Category))
if !validAssetCategory(category) {
return ProjectAssetSummary{}, BadAuthRequest("不支持的资产业务分类")
if req.Category != nil && req.FolderID != nil {
return ProjectAssetSummary{}, BadAuthRequest("一次只能修改素材分类或所在文件夹")
}
asset.Category = category
asset.UpdatedAt = time.Now()
if err := s.repo.UpdateAssetDomain(asset); err != nil {
return ProjectAssetSummary{}, err
if req.Category != nil {
category := model.AssetCategory(strings.TrimSpace(*req.Category))
if !validAssetCategory(category) {
return ProjectAssetSummary{}, BadAuthRequest("不支持的资产业务分类")
}
asset.Category = category
asset.UpdatedAt = time.Now()
if err := s.repo.UpdateAssetDomain(asset); err != nil {
return ProjectAssetSummary{}, err
}
if err := s.repo.BumpProjectRevision(projectID); err != nil {
return ProjectAssetSummary{}, err
}
} else if req.FolderID != nil {
folderID, folderErr := s.resolveProjectAssetFolderID(projectID, req.FolderID)
if folderErr != nil {
return ProjectAssetSummary{}, folderErr
}
position, positionErr := s.repo.NextProjectAssetPosition(projectID, folderID)
if positionErr != nil {
return ProjectAssetSummary{}, positionErr
}
if err := s.repo.MoveProjectAsset(projectID, asset.ID, folderID, position); err != nil {
return ProjectAssetSummary{}, err
}
} else {
return ProjectAssetSummary{}, BadAuthRequest("没有可更新的素材字段")
}
if err := s.repo.BumpProjectRevision(projectID); err != nil {
return ProjectAssetSummary{}, err
}
versions, err := s.repo.AssetVersions(asset.ID)
if err != nil {
return ProjectAssetSummary{}, err
}
usages, err := s.repo.ProjectAssetUsageRoles(projectID, asset.ID)
if err != nil {
return ProjectAssetSummary{}, err
}
return ProjectAssetSummary{ID: asset.ID, Title: asset.Title, MediaType: asset.Kind, Category: asset.Category, Status: asset.Status, PrimaryVersionID: asset.PrimaryVersionID, VersionCount: len(versions), Usages: usages, UpdatedAt: asset.UpdatedAt}, nil
return s.projectAssetSummary(userID, projectID, asset)
}
func (s *Service) CreateProjectAssetVersion(userID string, projectID string, assetID string, req CreateAssetVersionRequest) (model.AssetVersion, error) {
@@ -373,7 +382,15 @@ func (s *Service) ConfirmProjectAssetCandidate(userID string, projectID string,
candidate.Status = "confirmed"
candidate.ResolvedAssetID = assetID
candidate.UpdatedAt = now
link := model.ProjectAssetLink{ID: newID(), ProjectID: projectID, AssetID: assetID, CreatedAt: now}
folderID, folderErr := s.resolveProjectAssetFolderID(projectID, nil)
if folderErr != nil {
return ProjectAssetSummary{}, folderErr
}
position, positionErr := s.repo.NextProjectAssetPosition(projectID, folderID)
if positionErr != nil {
return ProjectAssetSummary{}, positionErr
}
link := model.ProjectAssetLink{ID: newID(), ProjectID: projectID, AssetID: assetID, FolderID: folderID, Position: position, CreatedAt: now}
if err := s.repo.ConfirmProjectAssetCandidate(candidate, &asset, &version, &link, createAsset); err != nil {
return ProjectAssetSummary{}, err
}
@@ -461,7 +478,12 @@ func (s *Service) projectAssetSummary(userID string, projectID string, asset *mo
if err != nil {
return ProjectAssetSummary{}, err
}
summary := ProjectAssetSummary{ID: asset.ID, Title: asset.Title, MediaType: asset.Kind, Category: asset.Category, Status: asset.Status, PrimaryVersionID: asset.PrimaryVersionID, VersionCount: len(versions), Usages: usages, UpdatedAt: asset.UpdatedAt}
link, err := s.repo.ProjectAssetLink(projectID, asset.ID)
if err != nil {
return ProjectAssetSummary{}, err
}
storageKey, previewText := projectAssetPreview(asset.PayloadJSON)
summary := ProjectAssetSummary{ID: asset.ID, Title: asset.Title, MediaType: asset.Kind, Category: asset.Category, Status: asset.Status, PrimaryVersionID: asset.PrimaryVersionID, VersionCount: len(versions), Usages: usages, FolderID: link.FolderID, Position: link.Position, StorageKey: storageKey, PreviewText: previewText, UpdatedAt: asset.UpdatedAt}
if asset.Category == model.AssetCategoryCharacter && asset.PrimaryVersionID != "" {
card, cardErr := s.characterCard(userID, asset)
if cardErr != nil {
@@ -471,3 +493,20 @@ func (s *Service) projectAssetSummary(userID string, projectID string, asset *mo
}
return summary, nil
}
func projectAssetPreview(payloadJSON string) (string, string) {
var payload struct {
Data struct {
StorageKey string `json:"storageKey"`
Content string `json:"content"`
} `json:"data"`
}
if json.Unmarshal([]byte(payloadJSON), &payload) != nil {
return "", ""
}
previewRunes := []rune(strings.TrimSpace(payload.Data.Content))
if len(previewRunes) > 240 {
previewRunes = append(previewRunes[:240], '…')
}
return strings.TrimSpace(payload.Data.StorageKey), string(previewRunes)
}
@@ -0,0 +1,256 @@
package service
import (
"errors"
"strings"
"time"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
)
const projectAssetFolderMaxDepth = 8
type CreateProjectAssetFolderRequest struct {
Name string `json:"name"`
ParentID string `json:"parentId"`
Style string `json:"style"`
}
type UpdateProjectAssetFolderRequest struct {
Name *string `json:"name"`
ParentID *string `json:"parentId"`
Style *string `json:"style"`
}
func (s *Service) ProjectAssetFolders(userID string, projectID string) ([]model.ProjectAssetFolder, error) {
if _, err := s.repo.ProjectForUser(userID, projectID); err != nil {
return nil, err
}
return s.repo.ProjectAssetFolders(projectID)
}
func (s *Service) CreateProjectAssetFolder(userID string, projectID string, req CreateProjectAssetFolderRequest) (model.ProjectAssetFolder, error) {
if _, err := s.repo.ProjectForUser(userID, projectID); err != nil {
return model.ProjectAssetFolder{}, err
}
folders, err := s.repo.ProjectAssetFolders(projectID)
if err != nil {
return model.ProjectAssetFolder{}, err
}
name, err := validateProjectAssetFolderName(req.Name)
if err != nil {
return model.ProjectAssetFolder{}, err
}
parentID := strings.TrimSpace(req.ParentID)
if err := validateProjectAssetFolderParent(folders, "", parentID); err != nil {
return model.ProjectAssetFolder{}, err
}
if projectAssetFolderNameExists(folders, parentID, name, "") {
return model.ProjectAssetFolder{}, BadAuthRequest("同级目录下已存在同名文件夹")
}
position := nextProjectAssetFolderPosition(folders, parentID)
now := time.Now()
style, err := validateProjectAssetFolderStyle(req.Style)
if err != nil {
return model.ProjectAssetFolder{}, err
}
folder := model.ProjectAssetFolder{ID: newID(), ProjectID: projectID, ParentID: parentID, Name: name, NameKey: projectAssetFolderNameKey(name), Style: style, Position: position, CreatedAt: now, UpdatedAt: now}
if err := s.repo.CreateProjectAssetFolder(&folder); err != nil {
if isProjectAssetFolderNameConflict(err) {
return model.ProjectAssetFolder{}, BadAuthRequest("同级目录下已存在同名文件夹")
}
return model.ProjectAssetFolder{}, err
}
return folder, nil
}
func (s *Service) UpdateProjectAssetFolder(userID string, projectID string, folderID string, req UpdateProjectAssetFolderRequest) (model.ProjectAssetFolder, error) {
if _, err := s.repo.ProjectForUser(userID, projectID); err != nil {
return model.ProjectAssetFolder{}, err
}
folder, err := s.repo.ProjectAssetFolder(projectID, strings.TrimSpace(folderID))
if err != nil {
return model.ProjectAssetFolder{}, err
}
folders, err := s.repo.ProjectAssetFolders(projectID)
if err != nil {
return model.ProjectAssetFolder{}, err
}
if req.Name != nil {
name, nameErr := validateProjectAssetFolderName(*req.Name)
if nameErr != nil {
return model.ProjectAssetFolder{}, nameErr
}
folder.Name = name
folder.NameKey = projectAssetFolderNameKey(name)
}
if req.ParentID != nil {
parentID := strings.TrimSpace(*req.ParentID)
if err := validateProjectAssetFolderParent(folders, folder.ID, parentID); err != nil {
return model.ProjectAssetFolder{}, err
}
if parentID != folder.ParentID {
folder.ParentID = parentID
folder.Position = nextProjectAssetFolderPosition(folders, parentID)
}
}
if req.Style != nil {
style, styleErr := validateProjectAssetFolderStyle(*req.Style)
if styleErr != nil {
return model.ProjectAssetFolder{}, styleErr
}
folder.Style = style
}
if projectAssetFolderNameExists(folders, folder.ParentID, folder.Name, folder.ID) {
return model.ProjectAssetFolder{}, BadAuthRequest("同级目录下已存在同名文件夹")
}
folder.UpdatedAt = time.Now()
if err := s.repo.UpdateProjectAssetFolder(folder); err != nil {
if isProjectAssetFolderNameConflict(err) {
return model.ProjectAssetFolder{}, BadAuthRequest("同级目录下已存在同名文件夹")
}
return model.ProjectAssetFolder{}, err
}
return *folder, nil
}
func (s *Service) DeleteProjectAssetFolder(userID string, projectID string, folderID string) error {
if _, err := s.repo.ProjectForUser(userID, projectID); err != nil {
return err
}
err := s.repo.DeleteProjectAssetFolder(projectID, strings.TrimSpace(folderID))
if errors.Is(err, repository.ErrProjectAssetFolderNotEmpty) {
return BadAuthRequest("文件夹非空,请先移动其中的素材和子文件夹")
}
return err
}
func (s *Service) resolveProjectAssetFolderID(projectID string, requested *string) (string, error) {
if requested == nil || strings.TrimSpace(*requested) == "" {
return "", nil
}
folderID := strings.TrimSpace(*requested)
if _, err := s.repo.ProjectAssetFolder(projectID, folderID); err != nil {
return "", BadAuthRequest("目标文件夹不存在或不属于当前素材库")
}
return folderID, nil
}
func validateProjectAssetFolderStyle(value string) (string, error) {
style := strings.TrimSpace(value)
if style == "" {
return "glass", nil
}
switch style {
case "glass", "stacked", "midnight", "paper", "cinema", "compact":
return style, nil
default:
return "", BadAuthRequest("不支持的文件夹样式")
}
}
func validateProjectAssetFolderName(value string) (string, error) {
name := strings.TrimSpace(value)
if name == "" {
return "", BadAuthRequest("文件夹名称不能为空")
}
if len([]rune(name)) > 60 {
return "", BadAuthRequest("文件夹名称不能超过 60 个字符")
}
return name, nil
}
func validateProjectAssetFolderParent(folders []model.ProjectAssetFolder, folderID string, parentID string) error {
byID := make(map[string]model.ProjectAssetFolder, len(folders))
for _, folder := range folders {
byID[folder.ID] = folder
}
ancestorDepth := 0
currentID := parentID
seen := make(map[string]struct{}, projectAssetFolderMaxDepth)
for currentID != "" {
if currentID == folderID && folderID != "" {
return BadAuthRequest("文件夹不能移动到自身或其子目录")
}
if _, exists := seen[currentID]; exists {
return BadAuthRequest("文件夹目录关系存在循环")
}
seen[currentID] = struct{}{}
current, ok := byID[currentID]
if !ok {
return BadAuthRequest("父文件夹不存在或不属于当前项目")
}
ancestorDepth++
currentID = current.ParentID
}
subtreeHeight := projectAssetFolderSubtreeHeight(folders, folderID)
if ancestorDepth+subtreeHeight > projectAssetFolderMaxDepth {
return BadAuthRequest("文件夹层级不能超过 8 层")
}
return nil
}
func projectAssetFolderSubtreeHeight(folders []model.ProjectAssetFolder, folderID string) int {
if folderID == "" {
return 1
}
maxDepth := 1
queue := []struct {
id string
depth int
}{{id: folderID, depth: 1}}
seen := map[string]struct{}{folderID: {}}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
if current.depth > maxDepth {
maxDepth = current.depth
}
for _, folder := range folders {
if folder.ParentID != current.id {
continue
}
if _, exists := seen[folder.ID]; exists {
continue
}
seen[folder.ID] = struct{}{}
queue = append(queue, struct {
id string
depth int
}{id: folder.ID, depth: current.depth + 1})
}
}
return maxDepth
}
func projectAssetFolderNameExists(folders []model.ProjectAssetFolder, parentID string, name string, excludeID string) bool {
for _, folder := range folders {
if folder.ID != excludeID && folder.ParentID == parentID && strings.EqualFold(folder.Name, name) {
return true
}
}
return false
}
func projectAssetFolderNameKey(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
func isProjectAssetFolderNameConflict(err error) bool {
if err == nil {
return false
}
message := strings.ToLower(err.Error())
return strings.Contains(message, "idx_project_asset_folders_sibling_name") || strings.Contains(message, "unique constraint") || strings.Contains(message, "duplicate key")
}
func nextProjectAssetFolderPosition(folders []model.ProjectAssetFolder, parentID string) int {
position := 0
for _, folder := range folders {
if folder.ParentID == parentID && folder.Position >= position {
position = folder.Position + 1
}
}
return position
}
@@ -0,0 +1,50 @@
package service
import (
"testing"
"infinite-canvas/backend/internal/model"
)
func TestValidateProjectAssetFolderParentRejectsCycle(t *testing.T) {
folders := []model.ProjectAssetFolder{
{ID: "root", ParentID: ""},
{ID: "child", ParentID: "root"},
{ID: "leaf", ParentID: "child"},
}
if err := validateProjectAssetFolderParent(folders, "root", "leaf"); err == nil {
t.Fatal("expected moving a folder into its descendant to fail")
}
}
func TestValidateProjectAssetFolderParentLimitsCombinedDepth(t *testing.T) {
folders := []model.ProjectAssetFolder{
{ID: "a", ParentID: ""},
{ID: "b", ParentID: "a"},
{ID: "c", ParentID: "b"},
{ID: "d", ParentID: "c"},
{ID: "e", ParentID: "d"},
{ID: "f", ParentID: "e"},
{ID: "g", ParentID: "f"},
{ID: "h", ParentID: "g"},
}
if err := validateProjectAssetFolderParent(folders, "", "h"); err == nil {
t.Fatal("expected a ninth level folder to fail")
}
if err := validateProjectAssetFolderParent(folders, "", "g"); err != nil {
t.Fatalf("expected an eighth level folder to be allowed: %v", err)
}
}
func TestProjectAssetFolderNameIsUniqueWithinSiblingOnly(t *testing.T) {
folders := []model.ProjectAssetFolder{
{ID: "first", ParentID: "one", Name: "Reference"},
{ID: "second", ParentID: "two", Name: "Reference"},
}
if !projectAssetFolderNameExists(folders, "one", "reference", "") {
t.Fatal("expected case-insensitive duplicate in the same parent")
}
if projectAssetFolderNameExists(folders, "three", "Reference", "") {
t.Fatal("did not expect a duplicate across different parents")
}
}
+13 -11
View File
@@ -136,7 +136,15 @@ func (s *Service) CreateProjectCharacter(userID string, projectID string, req Cr
}
asset := model.Asset{ID: assetID, UserID: userID, Kind: "entity", Category: model.AssetCategoryCharacter, Status: model.AssetVersionStatusConfirmed, PrimaryVersionID: versionID, Title: name, PayloadJSON: payload, CreatedAt: now, UpdatedAt: now}
version := model.AssetVersion{ID: versionID, AssetID: assetID, Version: 1, Status: model.AssetVersionStatusConfirmed, DefinitionJSON: string(definition), CreatedAt: now, UpdatedAt: now}
link := model.ProjectAssetLink{ID: newID(), ProjectID: projectID, AssetID: assetID, CreatedAt: now}
folderID, err := s.resolveProjectAssetFolderID(projectID, nil)
if err != nil {
return ProjectCharacterDetail{}, err
}
position, err := s.repo.NextProjectAssetPosition(projectID, folderID)
if err != nil {
return ProjectCharacterDetail{}, err
}
link := model.ProjectAssetLink{ID: newID(), ProjectID: projectID, AssetID: assetID, FolderID: folderID, Position: position, CreatedAt: now}
if err := s.repo.CreateProjectCharacter(projectID, &asset, &version, &link); err != nil {
return ProjectCharacterDetail{}, err
}
@@ -418,20 +426,14 @@ func (s *Service) prepareNextCharacterVersion(asset *model.Asset, name string, d
}
func (s *Service) projectCharacterDetail(userID string, projectID string, asset *model.Asset) (ProjectCharacterDetail, error) {
versions, err := s.repo.AssetVersions(asset.ID)
summary, err := s.projectAssetSummary(userID, projectID, asset)
if err != nil {
return ProjectCharacterDetail{}, err
}
usages, err := s.repo.ProjectAssetUsageRoles(projectID, asset.ID)
if err != nil {
return ProjectCharacterDetail{}, err
if summary.Character == nil {
return ProjectCharacterDetail{}, BadAuthRequest("角色素材缺少角色设定")
}
card, err := s.characterCard(userID, asset)
if err != nil {
return ProjectCharacterDetail{}, err
}
summary := ProjectAssetSummary{ID: asset.ID, Title: asset.Title, MediaType: asset.Kind, Category: asset.Category, Status: asset.Status, PrimaryVersionID: asset.PrimaryVersionID, VersionCount: len(versions), Usages: usages, UpdatedAt: asset.UpdatedAt, Character: &card}
return ProjectCharacterDetail{Asset: summary, Character: card}, nil
return ProjectCharacterDetail{Asset: summary, Character: *summary.Character}, nil
}
func (s *Service) characterCard(userID string, asset *model.Asset) (CharacterCardSummary, error) {