// Package store 维护本地 SQLite 台账。 // // 台账的职责只有一个:记录“哪些 LDAP 条目是我们创建的”。 // 这是安全删除的唯一依据——没有台账就没有删除权,因此 // 服务账号、手工创建的账号、admin 永远不会被同步任务误伤。 package store import ( "fmt" "time" "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // Account 是一个由同步器管理的用户账号。 type Account struct { // UID 即登录名(企业邮箱前缀),同时是主键。 UID string `gorm:"primaryKey;size:190"` DN string `gorm:"size:512;not null"` UnionID string `gorm:"size:190;index"` UserID string `gorm:"size:190"` Email string `gorm:"size:255"` Name string `gorm:"size:255"` // State: active(在 ou=people)或 inactive(在 ou=Inactive) State string `gorm:"size:16;not null;default:active"` // AttrsHash 是上一次写入的属性指纹,用于快速判断是否漂移 AttrsHash string `gorm:"size:64"` // DeptIDs 是所属部门,逗号分隔 DeptIDs string `gorm:"size:1024"` LastSyncedAt time.Time CreatedAt time.Time UpdatedAt time.Time } // Group 是一个由同步器管理的部门分组。 type Group struct { // DeptID 是飞书的 open_department_id,作为主键。 DeptID string `gorm:"primaryKey;size:190" json:"deptId"` DN string `gorm:"size:512;not null;uniqueIndex" json:"dn"` Name string `gorm:"size:255" json:"name"` ParentID string `gorm:"size:190;index" json:"parentId"` State string `gorm:"size:16;not null;default=active" json:"state"` LastSyncedAt time.Time `json:"lastSyncedAt"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // SyncRun 记录一次同步执行的结果,供历史页面展示。 type SyncRun struct { ID uint `gorm:"primaryKey" json:"id"` StartedAt time.Time `json:"startedAt"` FinishedAt time.Time `json:"finishedAt"` // Mode: dry-run 或 apply Mode string `gorm:"size:16" json:"mode"` Trigger string `gorm:"size:32" json:"trigger"` // manual 或 schedule Status string `gorm:"size:16" json:"status"` // success 或 failed Created int `json:"created"` Updated int `json:"updated"` Moved int `json:"moved"` Added int `json:"added"` // 加入分组次数 Removed int `json:"removed"` // 移出分组次数 Skipped int `json:"skipped"` Failed int `json:"failed"` ErrorMsg string `gorm:"size:2048" json:"errorMsg"` } // Store 是台账的访问入口。 type Store struct { db *gorm.DB } // Open 打开(必要时创建)SQLite 台账并完成建表。 func Open(path string) (*Store, error) { db, err := gorm.Open(sqlite.Open(path), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return nil, fmt.Errorf("打开台账数据库失败 (%s): %w", path, err) } // SQLite 在并发写时容易报 database is locked;限制为单连接可彻底避免。 sqlDB, err := db.DB() if err != nil { return nil, fmt.Errorf("获取数据库句柄失败: %w", err) } sqlDB.SetMaxOpenConns(1) if err := db.AutoMigrate(&MirrorUser{}, &MirrorDepartment{}, &MirrorUserDepartment{}, &ResignConfirmToken{}, &SyncRun{}, &Account{}, &Group{}); err != nil { return nil, fmt.Errorf("建表失败: %w", err) } return &Store{db: db}, nil } // Close 关闭底层连接。 func (s *Store) Close() error { sqlDB, err := s.db.DB() if err != nil { return err } return sqlDB.Close() } // DB 暴露底层句柄,供需要自定义查询的调用方使用。 func (s *Store) DB() *gorm.DB { return s.db }