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
+153
View File
@@ -0,0 +1,153 @@
package store
import (
"errors"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// --- 账号台账 ---
// AllAccounts 返回全部受管账号。
func (s *Store) AllAccounts() ([]*Account, error) {
var out []*Account
if err := s.db.Order("uid").Find(&out).Error; err != nil {
return nil, err
}
return out, nil
}
// AccountsByUID 返回 uid → 账号 的映射,供 diff 引擎使用。
func (s *Store) AccountsByUID() (map[string]*Account, error) {
list, err := s.AllAccounts()
if err != nil {
return nil, err
}
out := make(map[string]*Account, len(list))
for _, a := range list {
out[a.UID] = a
}
return out, nil
}
// UpsertAccount 写入或更新一条账号台账。
func (s *Store) UpsertAccount(a *Account) error {
a.LastSyncedAt = time.Now()
return s.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "uid"}},
DoUpdates: clause.AssignmentColumns([]string{
"dn", "union_id", "user_id", "email", "name",
"state", "attrs_hash", "dept_ids", "last_synced_at",
}),
}).Create(a).Error
}
// SetAccountState 更新账号状态与 DN(用于在职 ↔ 离职迁移)。
func (s *Store) SetAccountState(uid, state, dn string) error {
return s.db.Model(&Account{}).Where("uid = ?", uid).
Updates(map[string]any{"state": state, "dn": dn}).Error
}
// DeleteAccount 从台账删除一条账号。
func (s *Store) DeleteAccount(uid string) error {
return s.db.Where("uid = ?", uid).Delete(&Account{}).Error
}
// --- 分组台账 ---
// AllGroups 返回全部受管分组。
func (s *Store) AllGroups() ([]*Group, error) {
var out []*Group
if err := s.db.Order("dept_id").Find(&out).Error; err != nil {
return nil, err
}
return out, nil
}
// GroupsByDeptID 返回 dept_id → 分组 的映射。
func (s *Store) GroupsByDeptID() (map[string]*Group, error) {
list, err := s.AllGroups()
if err != nil {
return nil, err
}
out := make(map[string]*Group, len(list))
for _, g := range list {
out[g.DeptID] = g
}
return out, nil
}
// UpsertGroup 写入或更新一条分组台账。
func (s *Store) UpsertGroup(g *Group) error {
g.LastSyncedAt = time.Now()
return s.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "dept_id"}},
DoUpdates: clause.AssignmentColumns([]string{
"dn", "name", "parent_id", "state", "last_synced_at",
}),
}).Create(g).Error
}
// SetGroupState 更新分组状态。
func (s *Store) SetGroupState(deptID, state string) error {
return s.db.Model(&Group{}).Where("dept_id = ?", deptID).
Update("state", state).Error
}
// DeleteGroup 从台账删除一条分组。
func (s *Store) DeleteGroup(deptID string) error {
return s.db.Where("dept_id = ?", deptID).Delete(&Group{}).Error
}
// --- 同步记录 ---
// StartRun 创建一条同步记录并返回该记录。
//
// 返回完整记录而非仅 ID:调用方若自行构造一个只含 ID 的结构体再保存,
// GORM 的 Save 会把未赋值的 StartedAt/Mode/Trigger 一并写成零值,
// 导致历史页面显示 0001-01-01 与空白模式。
func (s *Store) StartRun(mode, trigger string) (*SyncRun, error) {
run := &SyncRun{
StartedAt: time.Now(),
Mode: mode,
Trigger: trigger,
Status: "running",
}
if err := s.db.Create(run).Error; err != nil {
return nil, err
}
return run, nil
}
// FinishRun 完成一条同步记录。
func (s *Store) FinishRun(run *SyncRun) error {
run.FinishedAt = time.Now()
return s.db.Save(run).Error
}
// RecentRuns 返回最近的同步记录,最新在前。
func (s *Store) RecentRuns(limit int) ([]*SyncRun, error) {
if limit <= 0 {
limit = 50
}
var out []*SyncRun
if err := s.db.Order("id desc").Limit(limit).Find(&out).Error; err != nil {
return nil, err
}
return out, nil
}
// LastRun 返回最近一次成功执行的记录,没有则返回 nil。
func (s *Store) LastRun() (*SyncRun, error) {
var out SyncRun
err := s.db.Where("status = ?", "success").Order("id desc").First(&out).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &out, nil
}