Files
idsync/internal/store/store.go
T
caowangrenbo 947b1a1475 chore: 初始化 idsync 项目
飞书通讯录 ↔ LDAP 同步工具的首个提交。

- internal/feishu: 飞书通讯录客户端、快照与重试
- internal/ldaplib: LDAP 读写与批量操作
- internal/mapping: 飞书部门/用户到 LDAP 条目的映射
- internal/syncer: 同步计划生成、执行与调度
- internal/store: SQLite 台账
- internal/server + internal/webui: 管理后台与内嵌前端产物
- internal/password: SSHA 口令处理
- config.example.yml / Dockerfile / Makefile / web 前端源码

真实凭据(config.yml)、运行时数据(data/、backup/)与构建产物(bin/)不入库。
2026-09-29 00:10:59 +08:00

107 lines
3.5 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 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(&Account{}, &Group{}, &SyncRun{}); 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 }