feat(canvas): 条件读取与历史 - 减少画布同步传输并校准版本

This commit is contained in:
ddcat666 committed 2026-09-29 08:26:29 +08:00
1 parent bcccddb4e9
commit 6ad1adc197
17 files changed
+783 -25

No files matched your search

+91 -10
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"time"
@@ -169,19 +170,99 @@ func buildCanvasSnapshot(before *model.CanvasProject, after model.CanvasProject,
// Row metadata is authoritative, including changes made by project association operations.
func canvasProjectPayload(project model.CanvasProject) (json.RawMessage, error) {
var payload map[string]json.RawMessage
if err := json.Unmarshal([]byte(project.PayloadJSON), &payload); err != nil {
return nil, err
metadata := []struct {
key string
value any
}{
{key: "id", value: project.ID},
{key: "title", value: project.Title},
{key: "projectId", value: project.ProjectID},
{key: "revision", value: project.Revision},
{key: "createdAt", value: project.CreatedAt},
{key: "updatedAt", value: project.UpdatedAt},
}
if payload == nil {
return nil, kernel.BadAuthRequest("画布数据格式错误")
}
for key, value := range map[string]any{"id": project.ID, "title": project.Title, "projectId": project.ProjectID, "revision": project.Revision, "createdAt": project.CreatedAt, "updatedAt": project.UpdatedAt} {
encoded, err := json.Marshal(value)
replacements := make(map[string]json.RawMessage, len(metadata))
for _, item := range metadata {
encoded, err := json.Marshal(item.value)
if err != nil {
return nil, err
}
payload[key] = encoded
replacements[item.key] = encoded
}
return json.Marshal(payload)
decoder := json.NewDecoder(strings.NewReader(project.PayloadJSON))
opening, err := decoder.Token()
if err != nil {
return nil, err
}
if opening != json.Delim('{') {
return nil, kernel.BadAuthRequest("画布数据格式错误")
}
result := make([]byte, 0, len(project.PayloadJSON)+128)
result = append(result, '{')
written := make(map[string]struct{}, len(replacements))
first := true
appendField := func(key string, value json.RawMessage) error {
if !first {
result = append(result, ',')
}
first = false
encodedKey, err := json.Marshal(key)
if err != nil {
return err
}
result = append(result, encodedKey...)
result = append(result, ':')
result = append(result, value...)
return nil
}
for decoder.More() {
token, err := decoder.Token()
if err != nil {
return nil, err
}
key, ok := token.(string)
if !ok {
return nil, kernel.BadAuthRequest("画布数据格式错误")
}
var value json.RawMessage
if err := decoder.Decode(&value); err != nil {
return nil, err
}
if replacement, exists := replacements[key]; exists {
if _, duplicate := written[key]; duplicate {
continue
}
value = replacement
written[key] = struct{}{}
}
if err := appendField(key, value); err != nil {
return nil, err
}
}
closing, err := decoder.Token()
if err != nil {
return nil, err
}
if closing != json.Delim('}') {
return nil, kernel.BadAuthRequest("画布数据格式错误")
}
var trailing json.RawMessage
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return nil, kernel.BadAuthRequest("画布数据格式错误")
}
return nil, err
}
for _, item := range metadata {
if _, exists := written[item.key]; exists {
continue
}
if err := appendField(item.key, replacements[item.key]); err != nil {
return nil, err
}
}
result = append(result, '}')
return json.RawMessage(result), nil
}
@@ -4,13 +4,55 @@ import (
"encoding/json"
"errors"
"net/http"
"reflect"
"strings"
"testing"
"time"
"infinite-canvas/backend/internal/kernel"
"infinite-canvas/backend/internal/model"
)
func TestCanvasProjectPayloadReplacesMetadataAndPreservesDocument(t *testing.T) {
project := model.CanvasProject{
ID: "canvas-authoritative",
Title: "Current title",
ProjectID: "project-current",
Revision: 9,
CreatedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
UpdatedAt: time.Date(2026, 2, 3, 4, 5, 6, 0, time.UTC),
PayloadJSON: `{"id":"stale","title":"stale","revision":1,"nodes":[ { "id": "node-1", "metadata": {"content":"large"} } ],"connections":[],"custom":{"preserve":true}}`,
}
raw, err := canvasProjectPayload(project)
if err != nil {
t.Fatal(err)
}
var actual map[string]json.RawMessage
if err := json.Unmarshal(raw, &actual); err != nil {
t.Fatal(err)
}
for key, want := range map[string]any{
"id": project.ID, "title": project.Title, "projectId": project.ProjectID,
"revision": project.Revision, "createdAt": project.CreatedAt, "updatedAt": project.UpdatedAt,
} {
var got any
if err := json.Unmarshal(actual[key], &got); err != nil {
t.Fatal(err)
}
encodedWant, _ := json.Marshal(want)
var decodedWant any
_ = json.Unmarshal(encodedWant, &decodedWant)
if !reflect.DeepEqual(got, decodedWant) {
t.Errorf("%s = %#v, want %#v", key, got, decodedWant)
}
}
for _, key := range []string{"nodes", "connections", "custom"} {
if len(actual[key]) == 0 || !json.Valid(actual[key]) {
t.Errorf("original %s payload was not preserved: %s", key, actual[key])
}
}
}
func TestCanvasHistoryRestore(t *testing.T) {
svc := newCanvasHistoryTestService(t)
actor := &model.User{ID: "owner"}
+4
View File
@@ -218,6 +218,10 @@ func (s *Service) UserCanvasProjectSummaries(userID string) ([]UserDataSummary,
return result, nil
}
func (s *Service) UserCanvasProjectMetadata(userID string, id string) (*model.CanvasProject, error) {
return s.repo.CanvasProjectMetadataForUser(userID, id)
}
func (s *Service) UserCanvasProject(userID string, id string) (json.RawMessage, error) {
project, err := s.repo.CanvasProjectForUser(userID, id)
if err != nil {