diff --git a/internal/api/handlers/scene.go b/internal/api/handlers/scene.go index b1b5283..b3ef8e8 100644 --- a/internal/api/handlers/scene.go +++ b/internal/api/handlers/scene.go @@ -434,6 +434,8 @@ func (h *SceneHandler) UpdateScene(c *gin.Context) { // Build update query dynamically updates := []string{} args := []interface{}{} + sceneNameForTasks := existing.Name + syncTaskSceneName := false if req.FactoryID != nil { factoryIDStr := strings.TrimSpace(*req.FactoryID) @@ -462,6 +464,8 @@ func (h *SceneHandler) UpdateScene(c *gin.Context) { if name != "" { updates = append(updates, "name = ?") args = append(args, name) + sceneNameForTasks = name + syncTaskSceneName = name != existing.Name } } @@ -503,6 +507,13 @@ func (h *SceneHandler) UpdateScene(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update scene"}) return } + if syncTaskSceneName { + if err := syncTaskSceneNameBySceneID(h.db, id, sceneNameForTasks); err != nil { + logger.Printf("[SCENE] Failed to sync task scene snapshot: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update scene"}) + return + } + } // Fetch the updated scene var s sceneRow diff --git a/internal/api/handlers/subscene.go b/internal/api/handlers/subscene.go index e36d3fe..e8f0531 100644 --- a/internal/api/handlers/subscene.go +++ b/internal/api/handlers/subscene.go @@ -545,6 +545,19 @@ func (h *SubsceneHandler) UpdateSubscene(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update subscene"}) return } + if effectiveSceneID != existing.SceneID || finalName != existing.Name { + var sceneName string + if err := h.db.Get(&sceneName, "SELECT name FROM scenes WHERE id = ? AND deleted_at IS NULL", effectiveSceneID); err != nil { + logger.Printf("[SUBSCENE] Failed to query scene name for task sync: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update subscene"}) + return + } + if err := syncTaskSubsceneSnapshotBySubsceneID(h.db, id, effectiveSceneID, sceneName, finalName); err != nil { + logger.Printf("[SUBSCENE] Failed to sync task subscene snapshot: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update subscene"}) + return + } + } // Fetch the updated subscene var s subsceneRow diff --git a/internal/api/handlers/task_snapshot_sync.go b/internal/api/handlers/task_snapshot_sync.go new file mode 100644 index 0000000..c33d0f6 --- /dev/null +++ b/internal/api/handlers/task_snapshot_sync.go @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 ArcheBase +// +// SPDX-License-Identifier: MulanPSL-2.0 + +package handlers + +import "database/sql" + +type taskSnapshotExecer interface { + Exec(query string, args ...interface{}) (sql.Result, error) +} + +func syncTaskSceneNameBySceneID(db taskSnapshotExecer, sceneID int64, sceneName string) error { + _, err := db.Exec( + `UPDATE tasks SET scene_name = ? WHERE scene_id = ? AND deleted_at IS NULL`, + sceneName, + sceneID, + ) + return err +} + +func syncTaskSubsceneSnapshotBySubsceneID(db taskSnapshotExecer, subsceneID int64, sceneID int64, sceneName string, subsceneName string) error { + _, err := db.Exec( + `UPDATE tasks + SET scene_id = ?, scene_name = ?, subscene_name = ? + WHERE subscene_id = ? AND deleted_at IS NULL`, + sceneID, + sceneName, + subsceneName, + subsceneID, + ) + return err +} diff --git a/internal/api/handlers/task_snapshot_sync_test.go b/internal/api/handlers/task_snapshot_sync_test.go new file mode 100644 index 0000000..5e3244b --- /dev/null +++ b/internal/api/handlers/task_snapshot_sync_test.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2026 ArcheBase +// +// SPDX-License-Identifier: MulanPSL-2.0 + +package handlers + +import ( + "testing" + + "github.com/jmoiron/sqlx" + _ "modernc.org/sqlite" +) + +func setupTaskSnapshotSyncDB(t *testing.T) *sqlx.DB { + t.Helper() + + db, err := sqlx.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("open sqlite db: %v", err) + } + t.Cleanup(func() { + if err := db.Close(); err != nil { + t.Fatalf("close sqlite db: %v", err) + } + }) + + _, err = db.Exec(` + CREATE TABLE tasks ( + id INTEGER PRIMARY KEY, + scene_id INTEGER, + subscene_id INTEGER, + scene_name TEXT, + subscene_name TEXT, + deleted_at TEXT + ) + `) + if err != nil { + t.Fatalf("create tasks table: %v", err) + } + return db +} + +func TestSyncTaskSceneNameBySceneID(t *testing.T) { + db := setupTaskSnapshotSyncDB(t) + if _, err := db.Exec(` + INSERT INTO tasks (id, scene_id, subscene_id, scene_name, subscene_name, deleted_at) VALUES + (1, 10, 20, 'old-scene', 'sub-a', NULL), + (2, 10, 21, 'old-scene', 'sub-b', '2026-01-01'), + (3, 11, 22, 'other-scene', 'sub-c', NULL) + `); err != nil { + t.Fatalf("insert tasks: %v", err) + } + + if err := syncTaskSceneNameBySceneID(db, 10, "new-scene"); err != nil { + t.Fatalf("syncTaskSceneNameBySceneID returned error: %v", err) + } + + rows := []struct { + ID int `db:"id"` + SceneName string `db:"scene_name"` + }{} + if err := db.Select(&rows, "SELECT id, scene_name FROM tasks ORDER BY id"); err != nil { + t.Fatalf("select tasks: %v", err) + } + want := map[int]string{1: "new-scene", 2: "old-scene", 3: "other-scene"} + for _, row := range rows { + if row.SceneName != want[row.ID] { + t.Fatalf("task %d scene_name = %q, want %q", row.ID, row.SceneName, want[row.ID]) + } + } +} + +func TestSyncTaskSubsceneSnapshotBySubsceneID(t *testing.T) { + db := setupTaskSnapshotSyncDB(t) + if _, err := db.Exec(` + INSERT INTO tasks (id, scene_id, subscene_id, scene_name, subscene_name, deleted_at) VALUES + (1, 10, 20, 'old-scene', 'old-subscene', NULL), + (2, 10, 20, 'old-scene', 'old-subscene', '2026-01-01'), + (3, 10, 21, 'old-scene', 'other-subscene', NULL) + `); err != nil { + t.Fatalf("insert tasks: %v", err) + } + + if err := syncTaskSubsceneSnapshotBySubsceneID(db, 20, 11, "new-scene", "new-subscene"); err != nil { + t.Fatalf("syncTaskSubsceneSnapshotBySubsceneID returned error: %v", err) + } + + rows := []struct { + ID int `db:"id"` + SceneID int `db:"scene_id"` + SceneName string `db:"scene_name"` + SubsceneName string `db:"subscene_name"` + }{} + if err := db.Select(&rows, "SELECT id, scene_id, scene_name, subscene_name FROM tasks ORDER BY id"); err != nil { + t.Fatalf("select tasks: %v", err) + } + + want := map[int]struct { + sceneID int + sceneName string + subsceneName string + }{ + 1: {sceneID: 11, sceneName: "new-scene", subsceneName: "new-subscene"}, + 2: {sceneID: 10, sceneName: "old-scene", subsceneName: "old-subscene"}, + 3: {sceneID: 10, sceneName: "old-scene", subsceneName: "other-subscene"}, + } + for _, row := range rows { + w := want[row.ID] + if row.SceneID != w.sceneID || row.SceneName != w.sceneName || row.SubsceneName != w.subsceneName { + t.Fatalf("task %d = (%d, %q, %q), want (%d, %q, %q)", + row.ID, + row.SceneID, + row.SceneName, + row.SubsceneName, + w.sceneID, + w.sceneName, + w.subsceneName, + ) + } + } +}