feat: 添加注释
This commit is contained in:
+85
-14
@@ -1,3 +1,23 @@
|
||||
// Package database 封装了本项目所有 SQLite 数据库的生命周期管理。
|
||||
//
|
||||
// # 架构设计说明
|
||||
//
|
||||
// 本项目采用"双 DB 模式",而非传统的"单一数据库":
|
||||
//
|
||||
// - settings.db — 全局设置库(AI 配置键值对 + 知识库注册表)
|
||||
// 存储在 os.UserConfigDir()/AI-Expert-Sidebar/ 下,随应用永久保留。
|
||||
//
|
||||
// - *.db — 知识库文件(每个业务话题一个独立 .db 文件)
|
||||
// 用户可随时创建/切换,彼此完全物理隔离,互不干扰。
|
||||
//
|
||||
// 这样设计的原因:传统单库方案依赖 user_id 做逻辑隔离,
|
||||
// 一旦查询漏加 WHERE 条件就会数据泄露;物理隔离则从根本上消除此风险,
|
||||
// 同时让用户可以直接拷贝/备份/分享单个 .db 文件,操作极为直观。
|
||||
//
|
||||
// # 并发安全
|
||||
//
|
||||
// 所有全局变量通过 sync.RWMutex 保护。读操作使用 RLock,
|
||||
// 仅写(切换库)时用 Lock,保证多 goroutine 并发安全。
|
||||
package database
|
||||
|
||||
import (
|
||||
@@ -15,20 +35,42 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
settingsDB *gorm.DB
|
||||
activeLib *gorm.DB
|
||||
// mu 保护下面所有包级变量的并发读写。
|
||||
mu sync.RWMutex
|
||||
|
||||
// settingsDB 是全局设置数据库,存储 AppSetting 和 Library 两张表。
|
||||
// 生命周期与应用相同,Init() 调用后即可使用。
|
||||
settingsDB *gorm.DB
|
||||
|
||||
// activeLib 指向当前正在使用的知识库 SQLite 连接。
|
||||
// 通过 OpenLibrary() 切换,Get() 读取。
|
||||
activeLib *gorm.DB
|
||||
|
||||
// activeLibNm 缓存当前活跃知识库的显示名称,供前端展示。
|
||||
// 避免每次都查数据库。
|
||||
activeLibNm string
|
||||
DataDir string
|
||||
|
||||
// DataDir 是应用数据目录的绝对路径。
|
||||
// 暴露给 service 层用于拼接新知识库 .db 文件路径。
|
||||
DataDir string
|
||||
)
|
||||
|
||||
// Init opens/creates the settings database ($HOME/Library/Application Support/AI-Expert-Sidebar/settings.db).
|
||||
// Init 初始化全局设置数据库。
|
||||
//
|
||||
// 具体操作:
|
||||
// 1. 调用 appDataDir() 确定存储目录(macOS: ~/Library/Application Support/AI-Expert-Sidebar/);
|
||||
// 2. 若目录不存在则创建(权限 0750,防止其他用户读取 API Key);
|
||||
// 3. 打开/创建 settings.db,并执行 AutoMigrate 建表;
|
||||
// 4. 若 settings.db 损坏或被删除,GORM 会重新创建,程序不会崩溃。
|
||||
//
|
||||
// 设计原则:Init 只负责"基础设施",不做业务初始化(那是 service.InitLibraries 的职责)。
|
||||
func Init() error {
|
||||
dir, err := appDataDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
DataDir = dir
|
||||
// 0o750: 当前用户 rwx,组 r-x,其他无权限 —— 保护含密钥的目录
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
return fmt.Errorf("create data dir: %w", err)
|
||||
}
|
||||
@@ -37,6 +79,7 @@ func Init() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("open settings.db: %w", err)
|
||||
}
|
||||
// AutoMigrate 是幂等的:若表已存在则只做增量列变更,不会删数据
|
||||
if err := db.AutoMigrate(&models.AppSetting{}, &models.Library{}); err != nil {
|
||||
return fmt.Errorf("migrate settings schema: %w", err)
|
||||
}
|
||||
@@ -47,12 +90,19 @@ func Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenLibrary switches the active knowledge library.
|
||||
// OpenLibrary 切换当前活跃的知识库到指定的 Library 记录所对应的 .db 文件。
|
||||
//
|
||||
// 调用时机:用户在 LibraryBar 下拉框中选择了某个知识库,
|
||||
// 或程序启动时 InitLibraries 恢复上次的选择。
|
||||
//
|
||||
// 注意:OpenLibrary 不关闭旧连接,GORM 底层连接池会自动管理;
|
||||
// 这使切换库操作无需等待旧连接关闭,响应更快。
|
||||
func OpenLibrary(lib models.Library) error {
|
||||
db, err := openSQLite(lib.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open library %q: %w", lib.Name, err)
|
||||
}
|
||||
// 每次打开都 AutoMigrate,保证旧版本数据库在新版 Entry schema 下能正常工作
|
||||
if err := db.AutoMigrate(&models.Entry{}); err != nil {
|
||||
return fmt.Errorf("migrate library %q: %w", lib.Name, err)
|
||||
}
|
||||
@@ -60,7 +110,7 @@ func OpenLibrary(lib models.Library) error {
|
||||
activeLib = db
|
||||
activeLibNm = lib.Name
|
||||
mu.Unlock()
|
||||
// Persist preference
|
||||
// 持久化"上次活跃库"偏好,使用 UPSERT 避免 duplicate key 错误
|
||||
settingsDB.Exec(
|
||||
"INSERT INTO app_settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||
"active_library", lib.Name,
|
||||
@@ -69,7 +119,10 @@ func OpenLibrary(lib models.Library) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewLibraryDB creates a fresh SQLite DB at path and migrates the Entry schema.
|
||||
// NewLibraryDB 在指定路径创建一个全新的知识库 SQLite 文件并建表。
|
||||
//
|
||||
// 物理创建而非逻辑创建——每个知识库是真实独立的文件,
|
||||
// 天然隔离,用户可直接在 Finder 中看到、备份或删除。
|
||||
func NewLibraryDB(path string) error {
|
||||
db, err := openSQLite(path)
|
||||
if err != nil {
|
||||
@@ -78,47 +131,65 @@ func NewLibraryDB(path string) error {
|
||||
return db.AutoMigrate(&models.Entry{})
|
||||
}
|
||||
|
||||
// NewLibraryDBReadOnly opens an existing SQLite DB read-only (for counting etc).
|
||||
// NewLibraryDBReadOnly 以只读模式打开一个已有的知识库文件。
|
||||
//
|
||||
// 专用于"条目计数"等查询场景,避免只读操作意外创建 WAL 日志文件
|
||||
// 或触发写锁,适合在列出知识库列表时并发调用。
|
||||
func NewLibraryDBReadOnly(path string) (*gorm.DB, error) {
|
||||
// ?mode=ro 是 SQLite URI 参数,让驱动以 SQLITE_OPEN_READONLY 打开
|
||||
return openSQLite(path + "?mode=ro")
|
||||
}
|
||||
|
||||
// GetSettings returns the global settings DB (AppSetting + Library tables).
|
||||
// GetSettings 返回全局设置数据库的 *gorm.DB 实例。
|
||||
// 调用方应先检查返回值是否为 nil(Init 尚未调用时可能为 nil)。
|
||||
func GetSettings() *gorm.DB {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return settingsDB
|
||||
}
|
||||
|
||||
// Get returns the active knowledge library DB.
|
||||
// Get 返回当前活跃知识库的 *gorm.DB 实例。
|
||||
// 在任何 CRUD 操作前都应先调用此函数,若返回 nil 则知识库尚未选定。
|
||||
func Get() *gorm.DB {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return activeLib
|
||||
}
|
||||
|
||||
// GetActiveLibName returns the display name of the currently open library.
|
||||
// GetActiveLibName 返回当前活跃知识库的显示名称(空字符串表示尚未打开)。
|
||||
func GetActiveLibName() string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return activeLibNm
|
||||
}
|
||||
|
||||
// IsReady reports whether both the settings DB and an active library are open.
|
||||
// IsReady 报告系统是否"就绪":设置库已初始化 且 有活跃知识库。
|
||||
// 前端通过 GetDBStatus() 轮询此函数,决定是否显示"本地 SQLite"绿点。
|
||||
func IsReady() bool {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return settingsDB != nil && activeLib != nil
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
// ── 内部辅助函数 ───────────────────────────────────────────────────────────────
|
||||
|
||||
// openSQLite 使用 glebarez/sqlite 纯 Go 驱动打开 SQLite 文件。
|
||||
//
|
||||
// 选用 glebarez/sqlite(而非 mattn/go-sqlite3)的原因:
|
||||
// 纯 Go 实现,无需 CGO,可以 GOOS=darwin/linux/windows 直接交叉编译,
|
||||
// 大幅简化 CI/CD 和分发流程,且兼容 GORM 接口。
|
||||
func openSQLite(path string) (*gorm.DB, error) {
|
||||
return gorm.Open(sqlite.Open(path), &gorm.Config{
|
||||
// Warn 级别:只记录慢查询和错误,避免调试日志污染生产输出
|
||||
Logger: logger.Default.LogMode(logger.Warn),
|
||||
})
|
||||
}
|
||||
|
||||
// appDataDir 返回应用推荐的数据目录。
|
||||
// macOS: ~/Library/Application Support/AI-Expert-Sidebar/
|
||||
// Linux: ~/.config/AI-Expert-Sidebar/
|
||||
// Windows: %AppData%\AI-Expert-Sidebar\
|
||||
// 使用 os.UserConfigDir() 而非硬编码路径,保证跨平台兼容。
|
||||
func appDataDir() (string, error) {
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user