package models import ( "time" ) // CelestiaSession represents a field record ("现场记录") session type CelestiaSession struct { ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` UserID string `gorm:"type:uuid;not null;index;uniqueIndex:idx_session_user_client" json:"userId"` ClientID *string `gorm:"size:100;uniqueIndex:idx_session_user_client" json:"clientId,omitempty"` Title string `gorm:"size:200;not null" json:"title"` StartTime time.Time `gorm:"not null" json:"startTime"` EndTime *time.Time `json:"endTime,omitempty"` DurationMs int64 `gorm:"default:0" json:"durationMs"` LocalAudioPath string `gorm:"size:500" json:"localAudioPath,omitempty"` IsSynced bool `gorm:"default:true" json:"isSynced"` PhotoCount int `gorm:"default:0" json:"photoCount"` NoteCount int `gorm:"default:0" json:"noteCount"` Events []TimelineEvent `gorm:"foreignKey:SessionID;constraint:OnDelete:CASCADE" json:"events"` Assets []MediaAsset `gorm:"foreignKey:SessionID;constraint:OnDelete:CASCADE" json:"assets"` Revision int64 `gorm:"default:1" json:"revision"` DeletedAt *time.Time `gorm:"index" json:"deletedAt,omitempty"` CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` } // SessionSyncIndexItem is the lightweight first phase of synchronization. // Events and asset descriptors are intentionally omitted; clients fetch the // full session only when UpdatedAt or Revision differs from local state. type SessionSyncIndexItem struct { ID string `json:"id"` ClientID *string `json:"clientId,omitempty"` Title string `json:"title"` StartTime time.Time `json:"startTime"` EndTime *time.Time `json:"endTime,omitempty"` DurationMs int64 `json:"durationMs"` PhotoCount int `json:"photoCount"` NoteCount int `json:"noteCount"` Revision int64 `json:"revision"` DeletedAt *time.Time `json:"deletedAt,omitempty"` UpdatedAt time.Time `json:"updatedAt"` } func (session CelestiaSession) SyncIndexItem() SessionSyncIndexItem { return SessionSyncIndexItem{ ID: session.ID, ClientID: session.ClientID, Title: session.Title, StartTime: session.StartTime, EndTime: session.EndTime, DurationMs: session.DurationMs, PhotoCount: session.PhotoCount, NoteCount: session.NoteCount, Revision: session.Revision, DeletedAt: session.DeletedAt, UpdatedAt: session.UpdatedAt, } } // TimelineEvent represents an event (NOTE, PHOTO, MARKER, VOICE) within a session type TimelineEvent struct { ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` SessionID string `gorm:"type:uuid;not null;index;uniqueIndex:idx_event_session_client" json:"sessionId"` ClientID *string `gorm:"size:100;uniqueIndex:idx_event_session_client" json:"clientId,omitempty"` RelativeTimeMs int64 `gorm:"not null" json:"relativeTimeMs"` EventType string `gorm:"size:20;not null" json:"eventType"` // PHOTO, NOTE, MARKER, VOICE TextContent string `gorm:"type:text" json:"textContent,omitempty"` LocalFilePath string `gorm:"size:500" json:"localFilePath,omitempty"` VoiceStartOffsetMs *int64 `json:"voiceStartOffsetMs,omitempty"` VoiceEndOffsetMs *int64 `json:"voiceEndOffsetMs,omitempty"` LocationName string `gorm:"size:200" json:"locationName,omitempty"` LocationAddress string `gorm:"size:500" json:"locationAddress,omitempty"` Latitude *float64 `json:"latitude,omitempty"` Longitude *float64 `json:"longitude,omitempty"` CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` } // MediaAsset is an uploaded audio or photo belonging to a session. StoragePath // is never serialized because it is an implementation detail of the server. type MediaAsset struct { ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` SessionID string `gorm:"type:uuid;not null;index;uniqueIndex:idx_asset_session_client" json:"sessionId"` UserID string `gorm:"type:uuid;not null;index" json:"userId"` ClientID string `gorm:"size:120;not null;uniqueIndex:idx_asset_session_client" json:"clientId"` Kind string `gorm:"size:20;not null" json:"kind"` FileName string `gorm:"size:255;not null" json:"fileName"` MIMEType string `gorm:"size:120" json:"mimeType"` SizeBytes int64 `gorm:"not null" json:"sizeBytes"` SHA256 string `gorm:"size:64;not null;index" json:"sha256"` StoragePath string `gorm:"size:1000;not null" json:"-"` CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` } type CreateSessionRequest struct { ClientID string `json:"clientId" binding:"required"` Title string `json:"title" binding:"required"` StartTime time.Time `json:"startTime" binding:"required"` EndTime *time.Time `json:"endTime"` DurationMs int64 `json:"durationMs"` LocalAudioPath string `json:"localAudioPath"` Events []CreateEventSubSchema `json:"events"` BaseRevision *int64 `json:"baseRevision"` DeletedEventClientIds []string `json:"deletedEventClientIds"` } type CreateEventSubSchema struct { ClientID string `json:"clientId" binding:"required"` RelativeTimeMs int64 `json:"relativeTimeMs"` EventType string `json:"eventType" binding:"required"` TextContent string `json:"textContent"` LocalFilePath string `json:"localFilePath"` VoiceStartOffsetMs *int64 `json:"voiceStartOffsetMs"` VoiceEndOffsetMs *int64 `json:"voiceEndOffsetMs"` LocationName string `json:"locationName"` LocationAddress string `json:"locationAddress"` Latitude *float64 `json:"latitude"` Longitude *float64 `json:"longitude"` } type CreateEventRequest struct { ClientID string `json:"clientId" binding:"required"` RelativeTimeMs int64 `json:"relativeTimeMs"` EventType string `json:"eventType" binding:"required"` TextContent string `json:"textContent"` LocalFilePath string `json:"localFilePath"` VoiceStartOffsetMs *int64 `json:"voiceStartOffsetMs"` VoiceEndOffsetMs *int64 `json:"voiceEndOffsetMs"` LocationName string `json:"locationName"` LocationAddress string `json:"locationAddress"` Latitude *float64 `json:"latitude"` Longitude *float64 `json:"longitude"` }