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/)不入库。
This commit is contained in:
2026-09-29 00:10:59 +08:00
commit 947b1a1475
43 changed files with 10079 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
// 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 }