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

171 lines
6.7 KiB
Go

package repository
import (
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func (r *Repository) ProjectCharacterAsset(userID string, projectID string, assetID string) (*model.Asset, error) {
var asset model.Asset
err := r.db.Table("assets").Select("assets.*").
Joins("JOIN project_asset_links ON project_asset_links.asset_id = assets.id").
Where("assets.id = ? AND assets.user_id = ? AND assets.category = ? AND project_asset_links.project_id = ?", assetID, userID, model.AssetCategoryCharacter, projectID).
First(&asset).Error
if err != nil {
return nil, err
}
return &asset, nil
}
func (r *Repository) AssetVersion(id string) (*model.AssetVersion, error) {
var version model.AssetVersion
if err := r.db.First(&version, "id = ?", id).Error; err != nil {
return nil, err
}
return &version, nil
}
func (r *Repository) AssetRepresentations(assetVersionID string) ([]model.AssetRepresentation, error) {
var representations []model.AssetRepresentation
err := r.db.Where("asset_version_id = ?", assetVersionID).Order("role asc, created_at asc").Find(&representations).Error
return representations, err
}
func (r *Repository) AssetRepresentationsForTask(taskID string) ([]model.AssetRepresentation, error) {
var representations []model.AssetRepresentation
err := r.db.Where("task_id = ?", taskID).Order("role asc, created_at asc").Find(&representations).Error
return representations, err
}
func (r *Repository) UnboundCharacterTurnaroundTasks(userID string, projectID string) ([]model.Task, error) {
var tasks []model.Task
err := r.db.Where("user_id = ? AND project_id = ? AND status = ? AND type = ?", userID, projectID, model.TaskStatusSucceeded, "canvas_image").
Where("input_json LIKE ?", "%character_turnaround%").
Where("NOT EXISTS (SELECT 1 FROM asset_representations WHERE asset_representations.task_id = tasks.id)").
Order("created_at asc").Limit(100).Find(&tasks).Error
return tasks, err
}
func (r *Repository) CharacterVoiceBinding(assetVersionID string) (*model.CharacterVoiceBinding, error) {
var binding model.CharacterVoiceBinding
if err := r.db.First(&binding, "asset_version_id = ?", assetVersionID).Error; err != nil {
return nil, err
}
return &binding, nil
}
func (r *Repository) VoiceProfileForUser(userID string, id string) (*model.VoiceProfile, error) {
var profile model.VoiceProfile
if err := r.db.First(&profile, "id = ? AND user_id = ?", id, userID).Error; err != nil {
return nil, err
}
return &profile, nil
}
func (r *Repository) VoiceProfiles(userID string) ([]model.VoiceProfile, error) {
var profiles []model.VoiceProfile
err := r.db.Where("user_id = ? AND status = ?", userID, "active").Order("created_at asc").Find(&profiles).Error
return profiles, err
}
func (r *Repository) EnsureVoiceProfiles(profiles []model.VoiceProfile) error {
if len(profiles) == 0 {
return nil
}
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "provider"}, {Name: "voice_key"}},
DoNothing: true,
}).Create(&profiles).Error
}
func (r *Repository) CreateVoiceProfile(profile *model.VoiceProfile) error {
return r.db.Create(profile).Error
}
func (r *Repository) VoiceProfileBySampleResource(userID string, resourceID string) (*model.VoiceProfile, error) {
var profile model.VoiceProfile
if err := r.db.First(&profile, "user_id = ? AND sample_resource_id = ? AND status = ?", userID, resourceID, "active").Error; err != nil {
return nil, err
}
return &profile, nil
}
// CreateProjectCharacter 将角色身份、首版本和项目关联放入同一事务。
func (r *Repository) CreateProjectCharacter(projectID string, asset *model.Asset, version *model.AssetVersion, link *model.ProjectAssetLink) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(asset).Error; err != nil {
return err
}
if err := tx.Create(version).Error; err != nil {
return err
}
if err := tx.Create(link).Error; err != nil {
return err
}
return tx.Model(&model.Project{}).Where("id = ?", projectID).
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": asset.UpdatedAt}).Error
})
}
// SaveCharacterVersion 通过不可变版本替换角色当前状态,旧镜头仍可继续引用历史版本。
func (r *Repository) SaveCharacterVersion(projectID string, asset *model.Asset, version *model.AssetVersion, representations []model.AssetRepresentation, voice *model.CharacterVoiceBinding) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := saveCharacterVersion(tx, asset, version, representations, voice); err != nil {
return err
}
return tx.Model(&model.Project{}).Where("id = ?", projectID).
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
})
}
// ConfirmProjectCharacterCandidate 保证角色归并版本与候选确认同时生效,失败时不会留下孤立版本。
func (r *Repository) ConfirmProjectCharacterCandidate(candidate *model.ProjectAssetCandidate, asset *model.Asset, version *model.AssetVersion, representations []model.AssetRepresentation, voice *model.CharacterVoiceBinding) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := saveCharacterVersion(tx, asset, version, representations, voice); err != nil {
return err
}
result := tx.Model(&model.ProjectAssetCandidate{}).
Where("id = ? AND project_id = ? AND status = ?", candidate.ID, candidate.ProjectID, "pending_confirmation").
Updates(map[string]any{"status": candidate.Status, "resolved_asset_id": candidate.ResolvedAssetID, "updated_at": candidate.UpdatedAt})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrInvalidData
}
return tx.Model(&model.Project{}).Where("id = ?", candidate.ProjectID).
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": candidate.UpdatedAt}).Error
})
}
func saveCharacterVersion(tx *gorm.DB, asset *model.Asset, version *model.AssetVersion, representations []model.AssetRepresentation, voice *model.CharacterVoiceBinding) error {
if err := tx.Create(version).Error; err != nil {
return err
}
if len(representations) > 0 {
if err := tx.Create(&representations).Error; err != nil {
return err
}
}
if voice != nil {
if err := tx.Create(voice).Error; err != nil {
return err
}
}
result := tx.Model(&model.Asset{}).Where("id = ? AND user_id = ?", asset.ID, asset.UserID).Updates(map[string]any{
"kind": asset.Kind, "category": asset.Category, "status": asset.Status, "primary_version_id": asset.PrimaryVersionID,
"title": asset.Title, "payload_json": asset.PayloadJSON, "updated_at": asset.UpdatedAt,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return gorm.ErrRecordNotFound
}
return nil
}