package dbsync import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "fmt" "sort" "strings" ) // FingerprintTable 单表指纹(与宇恒 sync_fingerprint 算法对齐)。 type FingerprintTable struct { Name string `json:"name"` PKColumn string `json:"pk_column"` RowCount int64 `json:"row_count"` ContentHash string `json:"content_hash"` Columns []string `json:"columns,omitempty"` Error string `json:"error,omitempty"` } // FingerprintRequest agent 拉线上表指纹。 type FingerprintRequest struct { OnlineDBID string `json:"online_db_id"` Tables []string `json:"tables,omitempty"` } // FingerprintResult 批量指纹。 type FingerprintResult struct { OK bool `json:"ok"` Tables []FingerprintTable `json:"tables"` } // FingerprintRemoteTables 计算线上 A 业务表 row_count + content_hash。 // 哈希:sha256( table+"|"+pk+"|" + Σ json.dumps(row, sort_keys=True)+"\n" ) 取前 32 hex。 func FingerprintRemoteTables(ctx context.Context, ch *Channel, req FingerprintRequest) (*FingerprintResult, error) { if ch == nil { return nil, fmt.Errorf("channel is nil") } db, err := AcquireRemote(ch.Remote.Driver, ch.Remote.DSN) if err != nil { return nil, wrapOpenRemote(err) } if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil { return nil, Retryablef("ensure meta: %v", err) } names, err := ListTables(ctx, db, ch.Remote.Driver) if err != nil { return nil, Retryablef("list tables: %v", err) } want := map[string]struct{}{} for _, t := range req.Tables { t = strings.TrimSpace(t) if t != "" { want[t] = struct{}{} } } out := &FingerprintResult{OK: true, Tables: make([]FingerprintTable, 0, len(names))} for _, name := range names { if strings.HasPrefix(name, "_ajz_") { continue } if len(want) > 0 { if _, ok := want[name]; !ok { continue } } if !tableInChannel(ch, name) { continue } fp, ferr := fingerprintOneTable(ctx, db, ch, name) if ferr != nil { fp.Error = ferr.Error() out.OK = false } out.Tables = append(out.Tables, fp) } return out, nil } func fingerprintOneTable(ctx context.Context, db *sql.DB, ch *Channel, table string) (FingerprintTable, error) { cols, err := listColumns(ctx, db, ch.Remote.Driver, table) if err != nil { return FingerprintTable{Name: table}, err } pkCol := "id" if ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[table]) != "" { pkCol = ch.PKColumns[table] } if !containsStr(cols, pkCol) { for _, c := range []string{"id", "__row_id", "_row_id"} { if containsStr(cols, c) { pkCol = c break } } if !containsStr(cols, pkCol) && len(cols) > 0 { pkCol = cols[0] } } n, err := countRows(ctx, db, ch.Remote.Driver, table) if err != nil { return FingerprintTable{Name: table, PKColumn: pkCol, Columns: cols}, err } hash, err := contentHashOrdered(ctx, db, ch.Remote.Driver, table, pkCol) if err != nil { return FingerprintTable{Name: table, PKColumn: pkCol, RowCount: n, Columns: cols}, err } return FingerprintTable{ Name: table, PKColumn: pkCol, RowCount: n, ContentHash: hash, Columns: cols, }, nil } func containsStr(ss []string, x string) bool { for _, s := range ss { if s == x { return true } } return false } func contentHashOrdered(ctx context.Context, db *sql.DB, driver Driver, table, pkCol string) (string, error) { q := fmt.Sprintf(`SELECT * FROM %s ORDER BY %s`, quoteIdent(driver, table), quoteIdent(driver, pkCol)) rows, err := db.QueryContext(ctx, q) if err != nil { return "", err } defer rows.Close() colNames, err := rows.Columns() if err != nil { return "", err } h := sha256.New() _, _ = h.Write([]byte(table + "|" + pkCol + "|")) ptrs := make([]any, len(colNames)) vals := make([]any, len(colNames)) for i := range vals { ptrs[i] = &vals[i] } for rows.Next() { for i := range vals { vals[i] = nil } if err := rows.Scan(ptrs...); err != nil { return "", err } m := make(map[string]any, len(colNames)) for i, c := range colNames { m[c] = normalizeSQLValue(vals[i]) } blob, err := stableJSON(m) if err != nil { return "", err } _, _ = h.Write(blob) _, _ = h.Write([]byte("\n")) } if err := rows.Err(); err != nil { return "", err } sum := hex.EncodeToString(h.Sum(nil)) if len(sum) > 32 { sum = sum[:32] } return sum, nil } // stableJSON 按 key 排序,贴近 Python json.dumps(..., sort_keys=True, default=str)。 func stableJSON(m map[string]any) ([]byte, error) { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { kb, err := json.Marshal(k) if err != nil { return nil, err } vb, err := json.Marshal(m[k]) if err != nil { // fallback string vb, err = json.Marshal(fmt.Sprint(m[k])) if err != nil { return nil, err } } parts = append(parts, string(kb)+":"+string(vb)) } return []byte("{" + strings.Join(parts, ",") + "}"), nil }