feat: ship loose-offline dbsync (validate, agent push, LWW audit)

Add UUID/FK channel checks, agent whitelist/push APIs, bindings, super-admin LWW audit with rollback, reconcile rate limits, and sync docs. Default customers stay opt-in; company conflict UI is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 17:54:14 +08:00
parent 632057c857
commit 76cdcd760e
39 changed files with 3302 additions and 199 deletions

View File

@@ -0,0 +1,79 @@
package dbsync
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
)
// RollbackLwwOverride 超管按落败快照回滚线上 A 单行。
// 仅 outcome=applied_source 可回滚:把 loser_payload 写回 remote无快照则按 pk 删除。
func RollbackLwwOverride(ctx context.Context, store *FileStore, overrideID string) (*LwwOverride, error) {
if store == nil {
return nil, fmt.Errorf("store is nil")
}
o, err := store.GetLwwOverride(overrideID)
if err != nil {
return nil, err
}
if o.Outcome != OutcomeApplied {
return nil, fmt.Errorf("仅「源端覆盖」记录可回滚(当前 outcome=%s", o.Outcome)
}
if strings.TrimSpace(o.ChannelID) == "" {
return nil, fmt.Errorf("override missing channel_id")
}
ch, err := store.GetChannel(o.ChannelID)
if err != nil {
return nil, fmt.Errorf("channel: %w", err)
}
table := strings.TrimSpace(o.Table)
pkCol := "id"
if ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[table]) != "" {
pkCol = ch.PKColumns[table]
}
db, err := Open(ch.Remote.Driver, ch.Remote.DSN)
if err != nil {
return nil, fmt.Errorf("open remote: %w", err)
}
defer db.Close()
if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil {
return nil, err
}
before := SnapshotTargetRow(ctx, db, ch.Remote.Driver, table, pkCol, o.RowPK)
ver := time.Now().UnixNano()
loser := strings.TrimSpace(o.LoserPayload)
op := "upsert"
payload := loser
if loser == "" || loser == "{}" {
op = "delete"
payload = fmt.Sprintf(`{%q:%q}`, pkCol, o.RowPK)
}
if err := ApplyChange(ctx, db, ch.Remote.Driver, table, pkCol, op, payload, ver); err != nil {
return nil, fmt.Errorf("apply rollback: %w", err)
}
rec := LwwOverride{
ID: uuid.NewString(),
TenantID: o.TenantID,
ChannelID: o.ChannelID,
Table: o.Table,
RowPK: o.RowPK,
Op: op,
Entry: EntryRollback,
Policy: "manual_rollback",
Outcome: OutcomeRolled,
LoserPayload: before,
WinnerPayload: payload,
TargetVer: o.SourceVer,
SourceVer: ver,
CreatedAt: time.Now().UTC(),
}
if err := store.AddLwwOverride(rec); err != nil {
return nil, err
}
return &rec, nil
}