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 }