package dbsync import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/google/uuid" ) // Binding 本机库 ↔ 线上库映射(登记/查询;用户自助 push 时按 user_id+online_db_id 鉴权)。 type Binding struct { ID string `json:"id"` TenantID int64 `json:"tenant_id"` UserID int64 `json:"user_id,omitempty"` LocalDatabaseID string `json:"local_database_id"` OnlineDBID string `json:"online_db_id"` ChannelID string `json:"channel_id,omitempty"` // 可读名(与宇恒 database_name / display_name 对齐;控制台优先展示) DatabaseName string `json:"database_name,omitempty"` // 本地库可读名 DisplayName string `json:"display_name,omitempty"` // 线上库可读名 Note string `json:"note,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (s *FileStore) bindingPath() string { return filepath.Join(s.dir, "bindings.json") } func (s *FileStore) EnsureBinding(b Binding) (Binding, error) { s.mu.Lock() defer s.mu.Unlock() list, err := s.readBindingsUnlocked() if err != nil { return b, err } b.LocalDatabaseID = strings.TrimSpace(b.LocalDatabaseID) b.OnlineDBID = strings.TrimSpace(b.OnlineDBID) b.DatabaseName = strings.TrimSpace(b.DatabaseName) b.DisplayName = strings.TrimSpace(b.DisplayName) if b.TenantID <= 0 { return b, fmt.Errorf("tenant_id required") } if b.LocalDatabaseID == "" || b.OnlineDBID == "" { return b, fmt.Errorf("local_database_id and online_db_id required") } now := time.Now().UTC() for i := range list { if list[i].TenantID == b.TenantID && list[i].LocalDatabaseID == b.LocalDatabaseID { if b.UserID > 0 && list[i].UserID > 0 && list[i].UserID != b.UserID { continue } list[i].OnlineDBID = b.OnlineDBID if b.ChannelID != "" { list[i].ChannelID = b.ChannelID } if b.DatabaseName != "" { list[i].DatabaseName = b.DatabaseName } if b.DisplayName != "" { list[i].DisplayName = b.DisplayName } if b.Note != "" { list[i].Note = b.Note } if b.UserID > 0 { list[i].UserID = b.UserID } list[i].UpdatedAt = now if err := s.writeBindingsUnlocked(list); err != nil { return b, err } return list[i], nil } } if b.ID == "" { b.ID = uuid.NewString() } b.CreatedAt = now b.UpdatedAt = now list = append(list, b) if err := s.writeBindingsUnlocked(list); err != nil { return b, err } return b, nil } func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Binding, error) { return s.ListBindingsFiltered(tenantID, 0, localDatabaseID) } // ListBindingsFiltered 按租户列出;userID>0 时仅返回该用户的 Binding。 func (s *FileStore) ListBindingsFiltered(tenantID, userID int64, localDatabaseID string) ([]Binding, error) { s.mu.Lock() defer s.mu.Unlock() list, err := s.readBindingsUnlocked() if err != nil { return nil, err } out := make([]Binding, 0) for _, b := range list { if b.TenantID != tenantID { continue } if userID > 0 && b.UserID != userID { continue } if localDatabaseID != "" && b.LocalDatabaseID != localDatabaseID { continue } out = append(out, b) } return out, nil } // UserOwnsOnlineDB 用户是否登记了该 online_db_id(可选限定 channel)。 func (s *FileStore) UserOwnsOnlineDB(tenantID, userID int64, channelID, onlineDBID string) bool { if tenantID <= 0 || userID <= 0 || strings.TrimSpace(onlineDBID) == "" { return false } list, err := s.ListBindingsFiltered(tenantID, userID, "") if err != nil { return false } online := strings.TrimSpace(onlineDBID) ch := strings.TrimSpace(channelID) for _, b := range list { if strings.TrimSpace(b.OnlineDBID) != online { continue } if ch != "" && b.ChannelID != "" && b.ChannelID != ch { continue } return true } return false } // UserCanAccessChannel 用户是否登记了指向该通道的 Binding(channel_id 空视为未限定通道)。 func (s *FileStore) UserCanAccessChannel(tenantID, userID int64, channelID string) bool { if tenantID <= 0 || userID <= 0 || strings.TrimSpace(channelID) == "" { return false } list, err := s.ListBindingsFiltered(tenantID, userID, "") if err != nil || len(list) == 0 { return false } ch := strings.TrimSpace(channelID) for _, b := range list { if b.ChannelID == "" || b.ChannelID == ch { return true } } return false } func (s *FileStore) GetBinding(tenantID int64, localDatabaseID string) (*Binding, error) { list, err := s.ListBindings(tenantID, localDatabaseID) if err != nil { return nil, err } if len(list) == 0 { return nil, fmt.Errorf("binding not found") } cp := list[0] return &cp, nil } func (s *FileStore) readBindingsUnlocked() ([]Binding, error) { path := s.bindingPath() b, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return []Binding{}, nil } return nil, err } if len(b) == 0 { return []Binding{}, nil } var list []Binding if err := json.Unmarshal(b, &list); err != nil { return nil, err } return list, nil } func (s *FileStore) writeBindingsUnlocked(list []Binding) error { path := s.bindingPath() raw, err := json.MarshalIndent(list, "", " ") if err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, raw, 0o600); err != nil { return err } return os.Rename(tmp, path) }