Files
idsync/internal/store/store.go
T
caowangrenbo 48b0ea35fc feat: store 镜像层(全字段镜像+部门关系+生命周期+一次性 token)
新增镜像存储层并保留旧台账过渡:
- MirrorUser/MirrorDepartment/MirrorUserDepartment/ResignConfirmToken 模型
- ReplaceMirror 单事务整体替换:用户/部门/关系一致更新,失败回滚保留旧版
- 生命周期状态机:active→leave_candidate→pending_confirm→confirmed_leave→deleted,
  非法转移结构性报错;快照只覆盖飞书事实不清洗状态
- 一次性离职确认 token:crypto/rand 生成、一次使用、过期作废、可批量 revoke
- 单测:事务回滚、状态机转移、token 一次/过期/复用被拒 全部通过
- docs/migration-baseline.md 记录阶段0基线字段
2026-09-29 23:19:47 +08:00

108 lines
3.6 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(&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 }