Files
ai_site/platform/internal/dbsync/binding.go
whm d195aa4804 feat: add Yuheng ticket bind, trial SMS off, shared bindings
Ship ticket-exchange and bind/policy for Z13, keep trial binds SMS-free, allow shared company bindings, and align SyncPage plus sync docs.

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

233 lines
6.0 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"`
// Shared公司共享库Z12c-2同租户任意成员可 push/pull 该 online_db_id
Shared bool `json:"shared,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].Shared = b.Shared
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或该库为公司 Shared。
func (s *FileStore) UserOwnsOnlineDB(tenantID, userID int64, channelID, onlineDBID string) bool {
if tenantID <= 0 || userID <= 0 || strings.TrimSpace(onlineDBID) == "" {
return false
}
online := strings.TrimSpace(onlineDBID)
ch := strings.TrimSpace(channelID)
own, err := s.ListBindingsFiltered(tenantID, userID, "")
if err == nil {
for _, b := range own {
if strings.TrimSpace(b.OnlineDBID) != online {
continue
}
if ch != "" && b.ChannelID != "" && b.ChannelID != ch {
continue
}
return true
}
}
all, err := s.ListBindingsFiltered(tenantID, 0, "")
if err != nil {
return false
}
for _, b := range all {
if !b.Shared {
continue
}
if strings.TrimSpace(b.OnlineDBID) != online {
continue
}
if ch != "" && b.ChannelID != "" && b.ChannelID != ch {
continue
}
return true
}
return false
}
// UserCanAccessChannel 用户是否登记了指向该通道的 Bindingchannel_id 空视为未限定通道);含 Shared。
func (s *FileStore) UserCanAccessChannel(tenantID, userID int64, channelID string) bool {
if tenantID <= 0 || userID <= 0 || strings.TrimSpace(channelID) == "" {
return false
}
ch := strings.TrimSpace(channelID)
list, err := s.ListBindingsFiltered(tenantID, userID, "")
if err == nil {
for _, b := range list {
if b.ChannelID == "" || b.ChannelID == ch {
return true
}
}
}
all, err := s.ListBindingsFiltered(tenantID, 0, "")
if err != nil {
return false
}
for _, b := range all {
if !b.Shared {
continue
}
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)
}