2 Achegas 23e5fd3125 ... fdb5bfc27b

Autor SHA1 Mensaxe Data
  bob.yuxinyang fdb5bfc27b fix(session): refresh updated_at timestamp on session and event mutations hai 1 mes
  bob.yuxinyang b78cc0f2fb feat(session): introduce lightweight SessionSyncIndexItem for incremental sync index hai 1 mes
Modificáronse 3 ficheiros con 98 adicións e 9 borrados
  1. 24 9
      handlers/session_handler.go
  2. 33 0
      models/session.go
  3. 41 0
      models/session_test.go

+ 24 - 9
handlers/session_handler.go

@@ -21,17 +21,15 @@ func sessionPreloads(db *gorm.DB) *gorm.DB {
 	}).Preload("Assets")
 }
 
-// GetSessions supports cursor-like incremental pulls with updatedAfter and can
-// include deletion tombstones for multi-device reconciliation.
+// GetSessions is the lightweight first phase of synchronization. It supports
+// cursor-like incremental pulls and deletion tombstones without preloading
+// concrete event or asset content.
 func (h *SessionHandler) GetSessions(c *gin.Context) {
 	userID := c.GetString("userID")
-	query := sessionPreloads(h.DB).Where("user_id = ?", userID)
+	query := h.DB.Where("user_id = ?", userID)
 	if c.Query("includeDeleted") != "true" {
 		query = query.Where("deleted_at IS NULL")
 	}
-	if search := c.Query("search"); search != "" {
-		query = query.Where("title ILIKE ?", "%"+search+"%")
-	}
 	if value := c.Query("updatedAfter"); value != "" {
 		updatedAfter, err := time.Parse(time.RFC3339Nano, value)
 		if err != nil {
@@ -45,7 +43,11 @@ func (h *SessionHandler) GetSessions(c *gin.Context) {
 		respondError(c, http.StatusInternalServerError, 500, "failed to query field records")
 		return
 	}
-	respondSuccess(c, sessions)
+	index := make([]models.SessionSyncIndexItem, 0, len(sessions))
+	for _, session := range sessions {
+		index = append(index, session.SyncIndexItem())
+	}
+	respondSuccess(c, index)
 }
 
 func (h *SessionHandler) GetSessionDetail(c *gin.Context) {
@@ -152,6 +154,12 @@ func (h *SessionHandler) CreateSession(c *gin.Context) {
 			}
 		}
 
+		// Events belong to the session's sync unit, so advance its timestamp
+		// after all event upserts/deletions have completed.
+		if err := tx.Model(&session).Update("updated_at", gorm.Expr("NOW()")).Error; err != nil {
+			return err
+		}
+
 		return nil
 	})
 	if isConflict {
@@ -238,7 +246,10 @@ func (h *SessionHandler) AddEventToSession(c *gin.Context) {
 		respondError(c, http.StatusInternalServerError, 500, "failed to add event")
 		return
 	}
-	h.DB.Model(&session).Updates(map[string]interface{}{"revision": gorm.Expr("revision + 1")})
+	h.DB.Model(&session).Updates(map[string]interface{}{
+		"revision":   gorm.Expr("revision + 1"),
+		"updated_at": gorm.Expr("NOW()"),
+	})
 	respondSuccess(c, event)
 }
 
@@ -249,7 +260,11 @@ func (h *SessionHandler) DeleteSession(c *gin.Context) {
 	now := time.Now().UTC()
 	result := h.DB.Model(&models.CelestiaSession{}).
 		Where("id = ? AND user_id = ? AND deleted_at IS NULL", sessionID, userID).
-		Updates(map[string]interface{}{"deleted_at": &now, "revision": gorm.Expr("revision + 1")})
+		Updates(map[string]interface{}{
+			"deleted_at": &now,
+			"revision":   gorm.Expr("revision + 1"),
+			"updated_at": gorm.Expr("NOW()"),
+		})
 	if result.Error != nil {
 		respondError(c, http.StatusInternalServerError, 500, "failed to delete record")
 		return

+ 33 - 0
models/session.go

@@ -25,6 +25,39 @@ type CelestiaSession struct {
 	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"`

+ 41 - 0
models/session_test.go

@@ -0,0 +1,41 @@
+package models
+
+import (
+	"encoding/json"
+	"strings"
+	"testing"
+	"time"
+)
+
+func TestSessionSyncIndexItemOmitsConcreteContent(t *testing.T) {
+	clientID := "client-1"
+	updatedAt := time.Date(2026, 7, 26, 12, 30, 0, 123000000, time.UTC)
+	session := CelestiaSession{
+		ID:         "session-1",
+		ClientID:   &clientID,
+		Title:      "现场记录",
+		StartTime:  updatedAt.Add(-time.Hour),
+		DurationMs: 3_600_000,
+		PhotoCount: 2,
+		NoteCount:  3,
+		Revision:   7,
+		UpdatedAt:  updatedAt,
+		Events:     []TimelineEvent{{ID: "event-1"}},
+		Assets:     []MediaAsset{{ID: "asset-1"}},
+	}
+
+	data, err := json.Marshal(session.SyncIndexItem())
+	if err != nil {
+		t.Fatalf("marshal sync index item: %v", err)
+	}
+	payload := string(data)
+	if strings.Contains(payload, `"events"`) || strings.Contains(payload, `"assets"`) {
+		t.Fatalf("sync index leaked concrete content: %s", payload)
+	}
+	if !strings.Contains(payload, `"updatedAt":"2026-07-26T12:30:00.123Z"`) {
+		t.Fatalf("sync index missing precise updatedAt: %s", payload)
+	}
+	if !strings.Contains(payload, `"revision":7`) {
+		t.Fatalf("sync index missing revision guard: %s", payload)
+	}
+}