Files
ai_site/platform/internal/dbsync/binding.go
whm b04b180d30 feat: harden loose-offline sync for user JWT, schema, and console ops
Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 09:47:35 +08:00

204 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 用户是否登记了指向该通道的 Bindingchannel_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)
}