package model import ( "html" "regexp" "strings" "time" "unicode/utf8" ) var projectUnitHTMLTagPattern = regexp.MustCompile(`<[^>]+>`) const ProjectUnitTitleMaxRunes = 240 func ProjectUnitWordCount(sourceText string) int { plainText := projectUnitHTMLTagPattern.ReplaceAllString(sourceText, "") return utf8.RuneCountInString(strings.TrimSpace(html.UnescapeString(plainText))) } func NormalizeProjectUnitTitle(value string) string { return strings.TrimSpace(value) } func ValidProjectUnitTitle(value string) bool { title := NormalizeProjectUnitTitle(value) return title != "" && utf8.RuneCountInString(title) <= ProjectUnitTitleMaxRunes } const AssetIDMaxLength = 80 type Resource struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36;index:idx_resources_user_created,priority:1;uniqueIndex:idx_resources_user_upload_key,priority:1"` Kind string `json:"kind" gorm:"index;size:24"` Status ResourceStatus `json:"status" gorm:"index;size:24"` Provider string `json:"provider" gorm:"size:24"` Endpoint string `json:"endpoint"` Bucket string `json:"bucket" gorm:"size:160"` // 用户 OSS 每次修改都会生成新版本,资源固定引用创建时的存储与密钥;只有同一存储位置才可复用当前 CDN。 StorageSettingID string `json:"-" gorm:"index;size:36"` ObjectKey string `json:"objectKey" gorm:"index"` PublicURL string `json:"publicUrl"` MimeType string `json:"mimeType" gorm:"size:120"` Size int64 `json:"size"` Width int `json:"width"` Height int `json:"height"` DurationMs int64 `json:"durationMs"` ETag string `json:"etag" gorm:"size:160"` // 浏览器兼容播放副本(HEVC/H.265 原片在 Chrome 等无法解码,由 ffmpeg 转 H.264): // PlaybackStatus: none|processing|ready|failed;PlaybackObjectKey 为本地播放目录下的文件名。 PlaybackStatus string `json:"playbackStatus" gorm:"index;size:24"` PlaybackObjectKey string `json:"playbackObjectKey"` PlaybackError string `json:"playbackError" gorm:"type:text"` // 单档画布展示缩略图;对象键固定由 ObjectKey 派生,不单独持久化。 ThumbnailStatus string `json:"thumbnailStatus" gorm:"size:24"` ThumbnailMimeType string `json:"thumbnailMimeType" gorm:"size:120"` ThumbnailSize int64 `json:"thumbnailSize"` ThumbnailWidth int `json:"thumbnailWidth"` ThumbnailHeight int `json:"thumbnailHeight"` // UploadKey 是客户端逻辑上传身份的摘要;NULL 表示不参与幂等约束。 UploadKey *string `json:"-" gorm:"size:64;uniqueIndex:idx_resources_user_upload_key,priority:2"` Error string `json:"error"` CreatedAt time.Time `json:"createdAt" gorm:"index:idx_resources_user_created,priority:2"` UpdatedAt time.Time `json:"updatedAt"` } // ResourceDeletionJob is the durable handoff between database deletion and // physical object cleanup. Storage fields are frozen because the Resource row // is removed in the same transaction that creates this job. type ResourceDeletionJob struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36"` ResourceID string `json:"resourceId" gorm:"index;size:36"` Provider string `json:"provider" gorm:"size:24"` Endpoint string `json:"endpoint"` Bucket string `json:"bucket" gorm:"size:160"` StorageSettingID string `json:"-" gorm:"index;size:36"` ObjectKey string `json:"objectKey" gorm:"index"` Status ResourceDeletionStatus `json:"status" gorm:"index:idx_resource_deletion_jobs_due,priority:1;size:24"` Attempts int `json:"attempts"` LastError string `json:"lastError" gorm:"type:text"` NextAttemptAt time.Time `json:"nextAttemptAt" gorm:"index:idx_resource_deletion_jobs_due,priority:2"` LeaseOwner string `json:"-" gorm:"index;size:120"` LeaseExpiresAt *time.Time `json:"-" gorm:"index"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // AnnouncementImageDraft marks an uploaded image as temporary until an // announcement create or update transaction consumes it. type AnnouncementImageDraft struct { ResourceID string `json:"resourceId" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36"` CreatedAt time.Time `json:"createdAt" gorm:"index"` } type Asset struct { ID string `json:"id" gorm:"primaryKey;size:80"` UserID string `json:"userId" gorm:"index;size:36;index:idx_assets_user_updated,priority:1"` FolderID string `json:"folderId,omitempty" gorm:"index;size:36"` Kind string `json:"kind" gorm:"index;size:24"` Category AssetCategory `json:"category" gorm:"index;size:32"` Status AssetVersionStatus `json:"status" gorm:"index;size:24"` PrimaryVersionID string `json:"primaryVersionId,omitempty" gorm:"index;size:36"` Title string `json:"title" gorm:"size:240"` PayloadJSON string `json:"payloadJson" gorm:"type:text"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt" gorm:"index:idx_assets_user_updated,priority:2"` } // AssetFolder 是用户素材库的一层自定义分类;业务分类仍由 Asset.Category 表达。 type AssetFolder struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36;uniqueIndex:idx_asset_folders_user_name,priority:1"` Name string `json:"name" gorm:"size:80"` NameKey string `json:"-" gorm:"size:80;uniqueIndex:idx_asset_folders_user_name,priority:2"` Position int `json:"position" gorm:"index"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type ProjectAssetLink struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;uniqueIndex:idx_project_asset_links_unique,priority:1;index:idx_project_asset_links_project_folder_position,priority:1"` AssetID string `json:"assetId" gorm:"index;size:80;uniqueIndex:idx_project_asset_links_unique,priority:2"` FolderID string `json:"folderId,omitempty" gorm:"index;size:36;index:idx_project_asset_links_project_folder_position,priority:2"` Position int `json:"position" gorm:"index;index:idx_project_asset_links_project_folder_position,priority:3"` CreatedAt time.Time `json:"createdAt"` } // ProjectAssetFolder 只保存项目内的目录结构;真实媒体仍由 Asset/Resource 唯一持有。 type ProjectAssetFolder struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;uniqueIndex:idx_project_asset_folders_sibling_name,priority:1"` ParentID string `json:"parentId,omitempty" gorm:"index;size:36;uniqueIndex:idx_project_asset_folders_sibling_name,priority:2"` Name string `json:"name" gorm:"size:240"` NameKey string `json:"-" gorm:"size:240;uniqueIndex:idx_project_asset_folders_sibling_name,priority:3"` Style string `json:"style" gorm:"size:24"` Theme string `json:"theme" gorm:"size:24"` Position int `json:"position" gorm:"index"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type ProjectAssetCandidate struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;index:idx_project_asset_candidates_project_unit_status,priority:1;index:idx_project_asset_candidates_project_status_category,priority:1"` UnitID string `json:"unitId,omitempty" gorm:"index;size:36;index:idx_project_asset_candidates_project_unit_status,priority:2"` ShotID string `json:"shotId,omitempty" gorm:"index;size:36"` Name string `json:"name" gorm:"size:240"` NameKey string `json:"-" gorm:"index;size:240"` Category AssetCategory `json:"category" gorm:"index;size:32;index:idx_project_asset_candidates_project_status_category,priority:3"` Status string `json:"status" gorm:"index;size:32;index:idx_project_asset_candidates_project_unit_status,priority:3;index:idx_project_asset_candidates_project_status_category,priority:2"` Source string `json:"source,omitempty" gorm:"index;size:48"` DetailsJSON string `json:"detailsJson" gorm:"type:text"` ResolvedAssetID string `json:"resolvedAssetId,omitempty" gorm:"index;size:80"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type AssetVersion struct { ID string `json:"id" gorm:"primaryKey;size:36"` AssetID string `json:"assetId" gorm:"index;size:80;uniqueIndex:idx_asset_versions_number,priority:1"` Version int `json:"version" gorm:"uniqueIndex:idx_asset_versions_number,priority:2"` Status AssetVersionStatus `json:"status" gorm:"index;size:24"` DefinitionJSON string `json:"definitionJson" gorm:"type:text"` Prompt string `json:"prompt" gorm:"type:text"` Note string `json:"note" gorm:"size:500"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type AssetRepresentation struct { ID string `json:"id" gorm:"primaryKey;size:36"` TaskID string `json:"taskId,omitempty" gorm:"index;size:36;uniqueIndex:idx_asset_representations_task_role,priority:1"` AssetVersionID string `json:"assetVersionId" gorm:"index;size:36;uniqueIndex:idx_asset_representations_version_role,priority:1"` ResourceID string `json:"resourceId,omitempty" gorm:"index;size:36"` MediaType string `json:"mediaType" gorm:"index;size:24"` Role string `json:"role" gorm:"index;size:32;uniqueIndex:idx_asset_representations_task_role,priority:2;uniqueIndex:idx_asset_representations_version_role,priority:2"` MetadataJSON string `json:"metadataJson" gorm:"type:text"` CreatedAt time.Time `json:"createdAt"` } // VoiceProfile 是可复用的声音身份;试听音频只是表现资源,不等同于声音本身。 type VoiceProfile struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36;uniqueIndex:idx_voice_profiles_user_provider_key,priority:1"` Name string `json:"name" gorm:"size:160"` Provider string `json:"provider" gorm:"size:48;uniqueIndex:idx_voice_profiles_user_provider_key,priority:2"` VoiceKey string `json:"voiceKey" gorm:"size:160;uniqueIndex:idx_voice_profiles_user_provider_key,priority:3"` Language string `json:"language" gorm:"size:80"` Timbre string `json:"timbre" gorm:"size:240"` SampleResourceID string `json:"sampleResourceId,omitempty" gorm:"index;size:36"` CompatibleModelsJSON string `json:"compatibleModelsJson" gorm:"type:text"` Status string `json:"status" gorm:"index;size:24"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type CharacterVoiceBinding struct { ID string `json:"id" gorm:"primaryKey;size:36"` AssetVersionID string `json:"assetVersionId" gorm:"uniqueIndex;size:36"` VoiceProfileID string `json:"voiceProfileId" gorm:"index;size:36"` Instructions string `json:"instructions" gorm:"type:text"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // Project 是短剧领域聚合根;CanvasProject 仍代表可独立创作的画布文档。 type Project struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36;uniqueIndex:idx_projects_user_name,priority:1"` Name string `json:"name" gorm:"size:240;uniqueIndex:idx_projects_user_name,priority:2"` Type string `json:"type" gorm:"size:32;index"` AspectRatio string `json:"aspectRatio" gorm:"size:16"` SourceType string `json:"sourceType" gorm:"size:32"` Description string `json:"description" gorm:"type:text"` CoverResourceID string `json:"coverResourceId,omitempty" gorm:"index;size:36"` StylePresetID string `json:"stylePresetId" gorm:"size:64"` StyleProfileJSON string `json:"styleProfileJson" gorm:"type:text"` DefaultImageModel string `json:"defaultImageModel,omitempty" gorm:"size:500"` DefaultVideoModel string `json:"defaultVideoModel,omitempty" gorm:"size:500"` Status ProjectStatus `json:"status" gorm:"index;size:24"` Revision int64 `json:"revision"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt" gorm:"index"` } // StyleProfile 是用户可持续编辑的风格源;项目只保存应用当时的 JSON 快照,避免源对象更新污染历史生成。 type StyleProfile struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36;index:idx_style_profiles_user_updated,priority:1"` Name string `json:"name" gorm:"size:160"` Description string `json:"description" gorm:"size:500"` CoverURL string `json:"coverUrl" gorm:"type:text"` TagsJSON string `json:"tagsJson" gorm:"type:text"` ProfileJSON string `json:"profileJson" gorm:"type:text"` Favorite bool `json:"favorite" gorm:"index"` LastUsedAt *time.Time `json:"lastUsedAt,omitempty" gorm:"index"` Revision int64 `json:"revision"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt" gorm:"index:idx_style_profiles_user_updated,priority:2"` } type ProjectUnit struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;index:idx_project_units_project_position,priority:1"` ParentID string `json:"parentId,omitempty" gorm:"index;size:36"` Kind ProjectUnitKind `json:"kind" gorm:"index;size:24"` Title string `json:"title" gorm:"size:240"` SourceText string `json:"sourceText" gorm:"type:text"` WordCount int `json:"wordCount"` Status ProjectUnitStatus `json:"status" gorm:"index;size:24"` Position int `json:"position" gorm:"index:idx_project_units_project_position,priority:2"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type CanvasUnitLink struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;uniqueIndex:idx_canvas_unit_links_unique,priority:1;index:idx_canvas_unit_links_project_unit,priority:1"` CanvasID string `json:"canvasId" gorm:"index;size:80;uniqueIndex:idx_canvas_unit_links_unique,priority:2"` UnitID string `json:"unitId" gorm:"index;size:36;uniqueIndex:idx_canvas_unit_links_unique,priority:3;index:idx_canvas_unit_links_project_unit,priority:2"` Role string `json:"role" gorm:"size:32"` CreatedAt time.Time `json:"createdAt"` } type Shot struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;index:idx_shots_project_unit_position,priority:1"` UnitID string `json:"unitId" gorm:"index;size:36;index:idx_shots_project_unit_position,priority:2"` CurrentRevisionID string `json:"currentRevisionId,omitempty" gorm:"index;size:36"` Title string `json:"title" gorm:"size:240"` Description string `json:"description" gorm:"type:text"` Position int `json:"position" gorm:"index:idx_shots_project_unit_position,priority:3"` DurationMs int64 `json:"durationMs"` Status string `json:"status" gorm:"index;size:24"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // ShotRevision 保存可复现的分镜脚本版本;Shot 只保留稳定身份、排序和当前版本指针。 type ShotRevision struct { ID string `json:"id" gorm:"primaryKey;size:36"` ShotID string `json:"shotId" gorm:"index;size:36;uniqueIndex:idx_shot_revisions_version,priority:1"` Version int `json:"version" gorm:"uniqueIndex:idx_shot_revisions_version,priority:2"` PlotDescription string `json:"plotDescription" gorm:"type:text"` Action string `json:"action" gorm:"type:text"` Dialogue string `json:"dialogue" gorm:"type:text"` ShotSize string `json:"shotSize" gorm:"size:80"` CameraAngle string `json:"cameraAngle" gorm:"size:80"` CameraMovement string `json:"cameraMovement" gorm:"size:120"` DurationMs int64 `json:"durationMs"` ImagePrompt string `json:"imagePrompt" gorm:"type:text"` VideoPrompt string `json:"videoPrompt" gorm:"type:text"` NegativePrompt string `json:"negativePrompt" gorm:"type:text"` ContinuityNotes string `json:"continuityNotes" gorm:"type:text"` ActionBeatsJSON string `json:"actionBeatsJson" gorm:"type:text"` CreatedBy string `json:"createdBy,omitempty" gorm:"index;size:36"` CreatedAt time.Time `json:"createdAt"` } // ShotArtifact 是镜头的版本化生产产物。修改分镜或资产引用时只标记 stale,不删除历史。 type ShotArtifact struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;index:idx_shot_artifacts_project_unit,priority:1"` UnitID string `json:"unitId" gorm:"index;size:36;index:idx_shot_artifacts_project_unit,priority:2"` ShotID string `json:"shotId" gorm:"index;size:36;uniqueIndex:idx_shot_artifacts_version,priority:1"` RevisionID string `json:"revisionId,omitempty" gorm:"index;size:36"` TaskID string `json:"taskId,omitempty" gorm:"index;size:36"` Type string `json:"type" gorm:"index;size:40;uniqueIndex:idx_shot_artifacts_version,priority:2"` Version int `json:"version" gorm:"uniqueIndex:idx_shot_artifacts_version,priority:3"` ResourceID string `json:"resourceId,omitempty" gorm:"index;size:36"` Status string `json:"status" gorm:"index;size:24"` Selected bool `json:"selected" gorm:"index"` MetadataJSON string `json:"metadataJson" gorm:"type:text"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type ShotAssetReference struct { ID string `json:"id" gorm:"primaryKey;size:36"` ShotID string `json:"shotId" gorm:"index;size:36;uniqueIndex:idx_shot_asset_reference_unique,priority:1"` AssetVersionID string `json:"assetVersionId" gorm:"index;size:36;uniqueIndex:idx_shot_asset_reference_unique,priority:2"` Role string `json:"role" gorm:"index;size:32;uniqueIndex:idx_shot_asset_reference_unique,priority:3"` Status string `json:"status" gorm:"index;size:24"` CreatedAt time.Time `json:"createdAt"` } type WorkflowTemplateVersion struct { ID string `json:"id" gorm:"primaryKey;size:36"` TemplateKey string `json:"templateKey" gorm:"size:80;uniqueIndex:idx_workflow_template_version,priority:1"` Name string `json:"name" gorm:"size:160"` Version int `json:"version" gorm:"uniqueIndex:idx_workflow_template_version,priority:2"` DefinitionJSON string `json:"definitionJson" gorm:"type:text"` CreatedAt time.Time `json:"createdAt"` } type WorkflowInstance struct { ID string `json:"id" gorm:"primaryKey;size:36"` ProjectID string `json:"projectId" gorm:"index;size:36;uniqueIndex:idx_workflow_instance_scope,priority:1"` UnitID string `json:"unitId,omitempty" gorm:"index;size:36;uniqueIndex:idx_workflow_instance_scope,priority:2"` TemplateVersionID string `json:"templateVersionId" gorm:"index;size:36;uniqueIndex:idx_workflow_instance_scope,priority:3"` Scope string `json:"scope" gorm:"index;size:24"` Status WorkflowStatus `json:"status" gorm:"index;size:24"` Revision int64 `json:"revision"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type WorkflowStepInstance struct { ID string `json:"id" gorm:"primaryKey;size:36"` WorkflowInstanceID string `json:"workflowInstanceId" gorm:"index;size:36;uniqueIndex:idx_workflow_steps_instance_key,priority:1"` StepKey string `json:"stepKey" gorm:"size:80;uniqueIndex:idx_workflow_steps_instance_key,priority:2"` Name string `json:"name" gorm:"size:160"` Position int `json:"position"` Status WorkflowStepStatus `json:"status" gorm:"index;size:24"` InputJSON string `json:"inputJson" gorm:"type:text"` OutputJSON string `json:"outputJson" gorm:"type:text"` Error string `json:"error" gorm:"type:text"` StartedAt *time.Time `json:"startedAt"` CompletedAt *time.Time `json:"completedAt"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type WorkflowStepTask struct { ID string `json:"id" gorm:"primaryKey;size:36"` WorkflowStepID string `json:"workflowStepId" gorm:"index;size:36;uniqueIndex:idx_workflow_step_tasks_unique,priority:1"` TaskID string `json:"taskId" gorm:"index;size:36;uniqueIndex:idx_workflow_step_tasks_unique,priority:2"` CreatedAt time.Time `json:"createdAt"` } // ProductionTaskLink 显式区分领域项目、画布和镜头上下文,避免继续复用 Task.ProjectID 表达多种身份。 type ProductionTaskLink struct { ID string `json:"id" gorm:"primaryKey;size:36"` TaskID string `json:"taskId" gorm:"index;size:36;uniqueIndex:idx_production_task_context,priority:1"` ProjectID string `json:"projectId" gorm:"index;size:36"` CanvasID string `json:"canvasId,omitempty" gorm:"index;size:80"` UnitID string `json:"unitId,omitempty" gorm:"index;size:36"` ShotID string `json:"shotId,omitempty" gorm:"index;size:36;uniqueIndex:idx_production_task_context,priority:2"` WorkflowStepID string `json:"workflowStepId,omitempty" gorm:"index;size:36"` ArtifactType string `json:"artifactType,omitempty" gorm:"index;size:40;uniqueIndex:idx_production_task_context,priority:3"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type CanvasProject struct { ID string `json:"id" gorm:"primaryKey;size:80"` UserID string `json:"userId" gorm:"index;size:36;index:idx_canvas_projects_user_updated,priority:1;index:idx_canvas_projects_user_project_updated,priority:1"` ProjectID string `json:"projectId,omitempty" gorm:"index;size:36;index:idx_canvas_projects_user_project_updated,priority:2"` Title string `json:"title" gorm:"size:240"` PayloadJSON string `json:"payloadJson" gorm:"type:text"` Revision int64 `json:"revision" gorm:"not null;default:1"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt" gorm:"index:idx_canvas_projects_user_updated,priority:2;index:idx_canvas_projects_user_project_updated,priority:3"` } type CanvasShare struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36;uniqueIndex:idx_canvas_share_owner_project,priority:1"` ProjectID string `json:"projectId" gorm:"index;size:80;uniqueIndex:idx_canvas_share_owner_project,priority:2"` TokenHash string `json:"-" gorm:"uniqueIndex;size:64"` TokenCipher string `json:"-" gorm:"type:text"` Enabled bool `json:"enabled" gorm:"index"` ExpiresAt *time.Time `json:"expiresAt" gorm:"index"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type PromptTemplate struct { ID string `json:"id" gorm:"primaryKey;size:36"` Operation string `json:"operation" gorm:"size:64;index;uniqueIndex:idx_prompt_template_operation_version,priority:1"` Name string `json:"name" gorm:"size:120"` Version int `json:"version" gorm:"uniqueIndex:idx_prompt_template_operation_version,priority:2"` Content string `json:"content" gorm:"type:text"` OutputType string `json:"outputType" gorm:"size:24"` Enabled bool `json:"enabled" gorm:"index;index:idx_prompt_template_active,priority:2"` CreatedBy string `json:"createdBy" gorm:"index;size:36"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // UserPromptCustomization 只保存用户的创作策略层,动态上下文和输出契约始终由服务端编译器注入。 type UserPromptCustomization struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"size:36;index;uniqueIndex:idx_user_prompt_operation,priority:1"` Operation string `json:"operation" gorm:"size:64;index;uniqueIndex:idx_user_prompt_operation,priority:2"` Mode string `json:"mode" gorm:"size:24"` Content string `json:"content" gorm:"type:text"` BaseTemplateID string `json:"baseTemplateId" gorm:"size:36;index"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type Announcement struct { ID string `json:"id" gorm:"primaryKey;size:36"` Title string `json:"title" gorm:"size:120"` Content string `json:"content" gorm:"type:text"` ImageResourceID string `json:"imageResourceId,omitempty" gorm:"index;size:36"` ImageURL string `json:"imageUrl,omitempty" gorm:"-"` Level AnnouncementLevel `json:"level" gorm:"index;size:24"` Pinned bool `json:"pinned" gorm:"index"` Status AnnouncementStatus `json:"status" gorm:"index;size:24;index:idx_announcements_status_published,priority:1"` CreatedBy string `json:"createdBy" gorm:"index;size:36"` PublishedAt time.Time `json:"publishedAt" gorm:"index:idx_announcements_status_published,priority:2"` ClosedAt *time.Time `json:"closedAt"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type UserAnnouncementRead struct { ID string `json:"id" gorm:"primaryKey;size:36"` UserID string `json:"userId" gorm:"index;size:36;uniqueIndex:idx_user_announcement_read,priority:1"` AnnouncementID string `json:"announcementId" gorm:"index;size:36;uniqueIndex:idx_user_announcement_read,priority:2"` ReadAt time.Time `json:"readAt"` } // BannerTitleRun 是通知标题中一段连续文本的样式覆盖;零值字段表示沿用默认样式。 // 字重与字号用指针区分「未设置」和「显式设置为默认值」。 type BannerTitleRun struct { Text string `json:"text"` FontSize *int `json:"fontSize,omitempty"` // px,仅接受 10-20 FontWeight *int `json:"fontWeight,omitempty"` // 仅接受 400 / 500 / 600 / 700 FontFamily string `json:"fontFamily,omitempty"` // "" | "sans" | "serif" | "mono" Color string `json:"color,omitempty"` // #RRGGBB } type BannerAnnouncement struct { ID string `json:"id" gorm:"primaryKey;size:36"` Title string `json:"title" gorm:"size:120"` // 纯文本标题,由 TitleRuns 拼接得出,供列表展示和关键字检索 // TitleRuns 是标题的样式分段,前端按段渲染;持久化在 title_runs 文本列,由仓储层显式编解码。 TitleRuns []BannerTitleRun `json:"titleRuns,omitempty" gorm:"-"` TitleRunsJSON string `json:"-" gorm:"column:title_runs;type:text"` // NoticeType 决定通知条底色("notice" | "activity" | "update" | "warning"),取值白名单见 app 层。 NoticeType string `json:"noticeType" gorm:"size:24"` Link string `json:"link" gorm:"size:500"` // 点击跳转目标:http(s) 外链或 / 开头的站内路径;空表示不可点击 Status string `json:"status" gorm:"size:24;index"` // "active" | "disabled" StartsAt *time.Time `json:"startsAt,omitempty" gorm:"index"` EndsAt *time.Time `json:"endsAt,omitempty" gorm:"index"` CreatedBy string `json:"createdBy" gorm:"size:36"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` }