✨ Use SQLite instead of JSON storage
This commit is contained in:
@@ -135,7 +135,7 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
TabSize: 4,
|
||||
TabType: TabTypeSpaces,
|
||||
// 保存选项
|
||||
AutoSaveDelay: 5000, // 5秒后自动保存
|
||||
AutoSaveDelay: 2000, // 2秒后自动保存
|
||||
},
|
||||
Appearance: AppearanceConfig{
|
||||
Language: LangZhCN,
|
||||
|
@@ -4,38 +4,27 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DocumentMeta 文档元数据
|
||||
type DocumentMeta struct {
|
||||
ID string `json:"id"` // 文档唯一标识
|
||||
Title string `json:"title"` // 文档标题
|
||||
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
// Document 表示一个文档
|
||||
// Document 表示一个文档(使用自增主键)
|
||||
type Document struct {
|
||||
Meta DocumentMeta `json:"meta"` // 元数据
|
||||
Content string `json:"content"` // 文档内容
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Content string `json:"content" db:"content"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
// DocumentInfo 文档信息(不包含内容,用于列表展示)
|
||||
type DocumentInfo struct {
|
||||
ID string `json:"id"` // 文档ID
|
||||
Title string `json:"title"` // 文档标题
|
||||
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
|
||||
Path string `json:"path"` // 文档路径
|
||||
// NewDocument 创建新文档(不需要传ID,由数据库自增)
|
||||
func NewDocument(title, content string) *Document {
|
||||
now := time.Now()
|
||||
return &Document{
|
||||
Title: title,
|
||||
Content: content,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefaultDocument 创建默认文档
|
||||
func NewDefaultDocument() *Document {
|
||||
now := time.Now()
|
||||
return &Document{
|
||||
Meta: DocumentMeta{
|
||||
ID: "default",
|
||||
Title: "默认文档",
|
||||
LastUpdated: now,
|
||||
CreatedAt: now,
|
||||
},
|
||||
Content: "∞∞∞text-a\n",
|
||||
}
|
||||
return NewDocument("default", "∞∞∞text-a\n")
|
||||
}
|
||||
|
@@ -132,7 +132,7 @@ func (cms *ConfigMigrationService[T]) checkResourceLimits() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// createBackupOptimized 优化的备份创建(单次扫描删除旧备份)
|
||||
// createBackupOptimized 优化的备份创建
|
||||
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
|
||||
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) {
|
||||
return "", nil
|
||||
@@ -155,7 +155,7 @@ func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
|
||||
return newBackupPath, copyFile(cms.configPath, newBackupPath)
|
||||
}
|
||||
|
||||
// tryQuickRecovery 快速恢复检查(避免完整的备份恢复)
|
||||
// tryQuickRecovery 快速恢复检查
|
||||
func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) {
|
||||
var testConfig T
|
||||
if existingConfig.Unmarshal("", &testConfig) != nil {
|
||||
@@ -213,7 +213,7 @@ func (cms *ConfigMigrationService[T]) mergeInPlace(existingConfig *koanf.Koanf,
|
||||
return false, fmt.Errorf("config merge failed: %w", err)
|
||||
}
|
||||
|
||||
// 更新元数据(直接操作,无需重新序列化)
|
||||
// 更新元数据
|
||||
mergeKoanf.Set("metadata.version", cms.targetVersion)
|
||||
mergeKoanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
||||
|
||||
@@ -249,12 +249,12 @@ func (cms *ConfigMigrationService[T]) atomicWrite(existingConfig *koanf.Koanf, c
|
||||
return existingConfig.Load(&BytesProvider{configBytes}, jsonparser.Parser())
|
||||
}
|
||||
|
||||
// fastMerge 快速合并函数(优化版本)
|
||||
// fastMerge 快速合并函数
|
||||
func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error {
|
||||
return cms.fastMergeRecursive(src, dest, 0)
|
||||
}
|
||||
|
||||
// fastMergeRecursive 快速递归合并(单次遍历,最小化反射使用)
|
||||
// fastMergeRecursive 快速递归合并
|
||||
func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error {
|
||||
if depth > MaxRecursionDepth {
|
||||
return fmt.Errorf("recursion depth exceeded")
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
@@ -28,6 +29,7 @@ type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig
|
||||
|
||||
// ConfigListener 配置监听器
|
||||
type ConfigListener struct {
|
||||
ID string // 监听器唯一ID
|
||||
Name string // 监听器名称
|
||||
ChangeType ConfigChangeType // 监听的配置变更类型
|
||||
Callback ConfigChangeCallback // 回调函数(现在包含新旧配置)
|
||||
@@ -45,9 +47,10 @@ type ConfigListener struct {
|
||||
|
||||
// ConfigNotificationService 配置通知服务
|
||||
type ConfigNotificationService struct {
|
||||
listeners sync.Map // 使用sync.Map替代普通map+锁
|
||||
logger *log.LoggerService // 日志服务
|
||||
koanf *koanf.Koanf // koanf实例
|
||||
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
||||
mu sync.RWMutex // 监听器map的读写锁
|
||||
logger *log.LoggerService // 日志服务
|
||||
koanf *koanf.Koanf // koanf实例
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
@@ -57,20 +60,19 @@ type ConfigNotificationService struct {
|
||||
func NewConfigNotificationService(k *koanf.Koanf, logger *log.LoggerService) *ConfigNotificationService {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &ConfigNotificationService{
|
||||
logger: logger,
|
||||
koanf: k,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
||||
logger: logger,
|
||||
koanf: k,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterListener 注册配置监听器
|
||||
func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error {
|
||||
// 清理已存在的监听器
|
||||
if existingValue, loaded := cns.listeners.LoadAndDelete(listener.ChangeType); loaded {
|
||||
if existing, ok := existingValue.(interface{ cancel() }); ok {
|
||||
existing.cancel()
|
||||
}
|
||||
// 生成唯一ID如果没有提供
|
||||
if listener.ID == "" {
|
||||
listener.ID = fmt.Sprintf("%s_%d", listener.Name, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// 初始化新监听器
|
||||
@@ -80,7 +82,11 @@ func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener)
|
||||
return fmt.Errorf("failed to initialize listener state: %w", err)
|
||||
}
|
||||
|
||||
cns.listeners.Store(listener.ChangeType, listener)
|
||||
// 添加到监听器列表
|
||||
cns.mu.Lock()
|
||||
cns.listeners[listener.ChangeType] = append(cns.listeners[listener.ChangeType], listener)
|
||||
cns.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -92,7 +98,7 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi
|
||||
|
||||
if config := listener.GetConfigFunc(cns.koanf); config != nil {
|
||||
listener.mu.Lock()
|
||||
listener.lastConfig = deepCopyConfig(config)
|
||||
listener.lastConfig = deepCopyConfigReflect(config)
|
||||
listener.lastConfigHash = computeConfigHash(config)
|
||||
listener.mu.Unlock()
|
||||
}
|
||||
@@ -100,23 +106,59 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterListener 注销配置监听器
|
||||
func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType) {
|
||||
if value, loaded := cns.listeners.LoadAndDelete(changeType); loaded {
|
||||
if listener, ok := value.(*ConfigListener); ok {
|
||||
// UnregisterListener 注销指定ID的配置监听器
|
||||
func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType, listenerID string) {
|
||||
cns.mu.Lock()
|
||||
defer cns.mu.Unlock()
|
||||
|
||||
listeners := cns.listeners[changeType]
|
||||
for i, listener := range listeners {
|
||||
if listener.ID == listenerID {
|
||||
// 取消监听器
|
||||
listener.cancel()
|
||||
// 从切片中移除
|
||||
cns.listeners[changeType] = append(listeners[:i], listeners[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果该类型没有监听器了,删除整个条目
|
||||
if len(cns.listeners[changeType]) == 0 {
|
||||
delete(cns.listeners, changeType)
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterAllListeners 注销指定类型的所有监听器
|
||||
func (cns *ConfigNotificationService) UnregisterAllListeners(changeType ConfigChangeType) {
|
||||
cns.mu.Lock()
|
||||
defer cns.mu.Unlock()
|
||||
|
||||
if listeners, exists := cns.listeners[changeType]; exists {
|
||||
for _, listener := range listeners {
|
||||
listener.cancel()
|
||||
}
|
||||
delete(cns.listeners, changeType)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckConfigChanges 检查配置变更并通知相关监听器
|
||||
func (cns *ConfigNotificationService) CheckConfigChanges() {
|
||||
cns.listeners.Range(func(key, value interface{}) bool {
|
||||
if listener, ok := value.(*ConfigListener); ok {
|
||||
cns.mu.RLock()
|
||||
allListeners := make(map[ConfigChangeType][]*ConfigListener)
|
||||
for changeType, listeners := range cns.listeners {
|
||||
// 创建监听器切片的副本以避免并发访问问题
|
||||
listenersCopy := make([]*ConfigListener, len(listeners))
|
||||
copy(listenersCopy, listeners)
|
||||
allListeners[changeType] = listenersCopy
|
||||
}
|
||||
cns.mu.RUnlock()
|
||||
|
||||
// 检查所有监听器
|
||||
for _, listeners := range allListeners {
|
||||
for _, listener := range listeners {
|
||||
cns.checkAndNotify(listener)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndNotify 检查配置变更并通知
|
||||
@@ -144,7 +186,7 @@ func (cns *ConfigNotificationService) checkAndNotify(listener *ConfigListener) {
|
||||
|
||||
if hasChanges {
|
||||
listener.mu.Lock()
|
||||
listener.lastConfig = deepCopyConfig(currentConfig)
|
||||
listener.lastConfig = deepCopyConfigReflect(currentConfig)
|
||||
listener.lastConfigHash = currentHash
|
||||
listener.mu.Unlock()
|
||||
|
||||
@@ -167,7 +209,82 @@ func computeConfigHash(config *models.AppConfig) string {
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
||||
// deepCopyConfig 深拷贝配置对象
|
||||
// deepCopyConfigReflect 使用反射实现高效深拷贝
|
||||
func deepCopyConfigReflect(src *models.AppConfig) *models.AppConfig {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用反射进行深拷贝
|
||||
srcValue := reflect.ValueOf(src).Elem()
|
||||
dstValue := reflect.New(srcValue.Type()).Elem()
|
||||
|
||||
deepCopyValue(srcValue, dstValue)
|
||||
|
||||
return dstValue.Addr().Interface().(*models.AppConfig)
|
||||
}
|
||||
|
||||
// deepCopyValue 递归深拷贝reflect.Value
|
||||
func deepCopyValue(src, dst reflect.Value) {
|
||||
switch src.Kind() {
|
||||
case reflect.Ptr:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
dst.Set(reflect.New(src.Elem().Type()))
|
||||
deepCopyValue(src.Elem(), dst.Elem())
|
||||
|
||||
case reflect.Struct:
|
||||
for i := 0; i < src.NumField(); i++ {
|
||||
if dst.Field(i).CanSet() {
|
||||
deepCopyValue(src.Field(i), dst.Field(i))
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Cap()))
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
deepCopyValue(src.Index(i), dst.Index(i))
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
dst.Set(reflect.MakeMap(src.Type()))
|
||||
for _, key := range src.MapKeys() {
|
||||
srcValue := src.MapIndex(key)
|
||||
dstValue := reflect.New(srcValue.Type()).Elem()
|
||||
deepCopyValue(srcValue, dstValue)
|
||||
dst.SetMapIndex(key, dstValue)
|
||||
}
|
||||
|
||||
case reflect.Interface:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
srcValue := src.Elem()
|
||||
dstValue := reflect.New(srcValue.Type()).Elem()
|
||||
deepCopyValue(srcValue, dstValue)
|
||||
dst.Set(dstValue)
|
||||
|
||||
case reflect.Array:
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
deepCopyValue(src.Index(i), dst.Index(i))
|
||||
}
|
||||
|
||||
default:
|
||||
// 对于基本类型和string,直接赋值
|
||||
if dst.CanSet() {
|
||||
dst.Set(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deepCopyConfig 保留原有的JSON深拷贝方法作为备用
|
||||
func deepCopyConfig(src *models.AppConfig) *models.AppConfig {
|
||||
if src == nil {
|
||||
return nil
|
||||
@@ -197,8 +314,8 @@ func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, o
|
||||
}
|
||||
|
||||
// 创建配置副本,避免在闭包中持有原始引用
|
||||
oldConfigCopy := deepCopyConfig(oldConfig)
|
||||
newConfigCopy := deepCopyConfig(newConfig)
|
||||
oldConfigCopy := deepCopyConfigReflect(oldConfig)
|
||||
newConfigCopy := deepCopyConfigReflect(newConfig)
|
||||
|
||||
changeType := listener.ChangeType
|
||||
|
||||
@@ -246,18 +363,33 @@ func (cns *ConfigNotificationService) executeCallback(
|
||||
func (cns *ConfigNotificationService) Cleanup() {
|
||||
cns.cancel() // 取消所有context
|
||||
|
||||
cns.listeners.Range(func(key, value interface{}) bool {
|
||||
cns.listeners.Delete(key)
|
||||
return true
|
||||
})
|
||||
cns.mu.Lock()
|
||||
for changeType, listeners := range cns.listeners {
|
||||
for _, listener := range listeners {
|
||||
listener.cancel()
|
||||
}
|
||||
delete(cns.listeners, changeType)
|
||||
}
|
||||
cns.mu.Unlock()
|
||||
|
||||
cns.wg.Wait() // 等待所有协程完成
|
||||
}
|
||||
|
||||
// GetListeners 获取指定类型的所有监听器
|
||||
func (cns *ConfigNotificationService) GetListeners(changeType ConfigChangeType) []*ConfigListener {
|
||||
cns.mu.RLock()
|
||||
defer cns.mu.RUnlock()
|
||||
|
||||
listeners := cns.listeners[changeType]
|
||||
result := make([]*ConfigListener, len(listeners))
|
||||
copy(result, listeners)
|
||||
return result
|
||||
}
|
||||
|
||||
// CreateHotkeyListener 创建热键配置监听器
|
||||
func CreateHotkeyListener(callback func(enable bool, hotkey *models.HotkeyCombo) error) *ConfigListener {
|
||||
func CreateHotkeyListener(name string, callback func(enable bool, hotkey *models.HotkeyCombo) error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: "HotkeyListener",
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeHotkey,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
if newConfig != nil {
|
||||
@@ -279,9 +411,9 @@ func CreateHotkeyListener(callback func(enable bool, hotkey *models.HotkeyCombo)
|
||||
}
|
||||
|
||||
// CreateDataPathListener 创建数据路径配置监听器
|
||||
func CreateDataPathListener(callback func(oldPath, newPath string) error) *ConfigListener {
|
||||
func CreateDataPathListener(name string, callback func() error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: "DataPathListener",
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeDataPath,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
var oldPath, newPath string
|
||||
@@ -298,7 +430,7 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi
|
||||
}
|
||||
|
||||
if oldPath != newPath {
|
||||
return callback(oldPath, newPath)
|
||||
return callback()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -313,8 +445,8 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
||||
// OnShutdown 关闭服务
|
||||
func (cns *ConfigNotificationService) OnShutdown() error {
|
||||
cns.Cleanup()
|
||||
return nil
|
||||
}
|
||||
|
@@ -290,22 +290,22 @@ func (cs *ConfigService) SetHotkeyChangeCallback(callback func(enable bool, hotk
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 创建热键监听器并注册
|
||||
hotkeyListener := CreateHotkeyListener(callback)
|
||||
hotkeyListener := CreateHotkeyListener("DefaultHotkeyListener", callback)
|
||||
return cs.notificationService.RegisterListener(hotkeyListener)
|
||||
}
|
||||
|
||||
// SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||
func (cs *ConfigService) SetDataPathChangeCallback(callback func(oldPath, newPath string) error) error {
|
||||
func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 创建数据路径监听器并注册
|
||||
dataPathListener := CreateDataPathListener(callback)
|
||||
dataPathListener := CreateDataPathListener("DefaultDataPathListener", callback)
|
||||
return cs.notificationService.RegisterListener(dataPathListener)
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (cs *ConfigService) ServiceShutdown() error {
|
||||
// OnShutdown 关闭服务
|
||||
func (cs *ConfigService) OnShutdown() error {
|
||||
cs.stopWatching()
|
||||
if cs.notificationService != nil {
|
||||
cs.notificationService.Cleanup()
|
||||
|
@@ -2,270 +2,337 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
_ "modernc.org/sqlite" // SQLite driver
|
||||
)
|
||||
|
||||
// DocumentService 提供文档管理功能
|
||||
// SQL constants for database operations
|
||||
const (
|
||||
dbName = "voidraft.db"
|
||||
// Database schema (simplified single table with auto-increment ID)
|
||||
sqlCreateDocumentsTable = `
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT DEFAULT '∞∞∞text-a',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`
|
||||
|
||||
// Performance optimization indexes
|
||||
sqlCreateIndexUpdatedAt = `CREATE INDEX IF NOT EXISTS idx_documents_updated_at ON documents(updated_at DESC)`
|
||||
sqlCreateIndexTitle = `CREATE INDEX IF NOT EXISTS idx_documents_title ON documents(title)`
|
||||
|
||||
// SQLite performance optimization settings
|
||||
sqlOptimizationSettings = `
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA cache_size = -64000;
|
||||
PRAGMA temp_store = MEMORY;
|
||||
PRAGMA foreign_keys = ON;`
|
||||
|
||||
// Document operations
|
||||
sqlGetDocumentByID = `
|
||||
SELECT id, title, content, created_at, updated_at
|
||||
FROM documents
|
||||
WHERE id = ?`
|
||||
|
||||
sqlInsertDocument = `
|
||||
INSERT INTO documents (title, content, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
|
||||
sqlUpdateDocument = `
|
||||
UPDATE documents
|
||||
SET title = ?, content = ?, updated_at = ?
|
||||
WHERE id = ?`
|
||||
|
||||
sqlUpdateDocumentContent = `
|
||||
UPDATE documents
|
||||
SET content = ?, updated_at = ?
|
||||
WHERE id = ?`
|
||||
|
||||
sqlUpdateDocumentTitle = `
|
||||
UPDATE documents
|
||||
SET title = ?, updated_at = ?
|
||||
WHERE id = ?`
|
||||
|
||||
sqlDeleteDocument = `
|
||||
DELETE FROM documents WHERE id = ?`
|
||||
|
||||
sqlListAllDocumentsMeta = `
|
||||
SELECT id, title, created_at, updated_at
|
||||
FROM documents
|
||||
ORDER BY updated_at DESC`
|
||||
|
||||
sqlGetFirstDocumentID = `
|
||||
SELECT id FROM documents ORDER BY id LIMIT 1`
|
||||
)
|
||||
|
||||
// DocumentService provides document management functionality
|
||||
type DocumentService struct {
|
||||
configService *ConfigService
|
||||
logger *log.LoggerService
|
||||
docStore *Store[models.Document]
|
||||
|
||||
// 文档状态管理
|
||||
mu sync.RWMutex
|
||||
document *models.Document
|
||||
|
||||
// 自动保存管理
|
||||
db *sql.DB
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
isDirty atomic.Bool
|
||||
lastSaveTime atomic.Int64 // unix timestamp
|
||||
saveScheduler chan struct{}
|
||||
|
||||
// 初始化控制
|
||||
initOnce sync.Once
|
||||
}
|
||||
|
||||
// NewDocumentService 创建文档服务
|
||||
// NewDocumentService creates a new document service
|
||||
func NewDocumentService(configService *ConfigService, logger *log.LoggerService) *DocumentService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &DocumentService{
|
||||
configService: configService,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
saveScheduler: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (ds *DocumentService) Initialize() error {
|
||||
var initErr error
|
||||
ds.initOnce.Do(func() {
|
||||
initErr = ds.doInitialize()
|
||||
})
|
||||
return initErr
|
||||
// OnStartup initializes the service when the application starts
|
||||
func (ds *DocumentService) OnStartup(ctx context.Context, _ application.ServiceOptions) error {
|
||||
ds.ctx = ctx
|
||||
return ds.initDatabase()
|
||||
}
|
||||
|
||||
// doInitialize 执行初始化
|
||||
func (ds *DocumentService) doInitialize() error {
|
||||
if err := ds.initStore(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds.loadDocument()
|
||||
go ds.autoSaveWorker()
|
||||
return nil
|
||||
}
|
||||
|
||||
// initStore 初始化存储
|
||||
func (ds *DocumentService) initStore() error {
|
||||
docPath, err := ds.getDocumentPath()
|
||||
// initDatabase initializes the SQLite database
|
||||
func (ds *DocumentService) initDatabase() error {
|
||||
dbPath, err := ds.getDatabasePath()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get database path: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(docPath), 0755); err != nil {
|
||||
return err
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
ds.docStore = NewStore[models.Document](StoreOption{
|
||||
FilePath: docPath,
|
||||
AutoSave: false,
|
||||
Logger: ds.logger,
|
||||
})
|
||||
ds.db = db
|
||||
|
||||
// Apply optimization settings
|
||||
if _, err := db.Exec(sqlOptimizationSettings); err != nil {
|
||||
return fmt.Errorf("failed to apply optimization settings: %w", err)
|
||||
}
|
||||
|
||||
// Create table
|
||||
if _, err := db.Exec(sqlCreateDocumentsTable); err != nil {
|
||||
return fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
if err := ds.createIndexes(); err != nil {
|
||||
return fmt.Errorf("failed to create indexes: %w", err)
|
||||
}
|
||||
|
||||
// Ensure default document exists
|
||||
if err := ds.ensureDefaultDocument(); err != nil {
|
||||
return fmt.Errorf("failed to ensure default document: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDocumentPath 获取文档路径
|
||||
func (ds *DocumentService) getDocumentPath() (string, error) {
|
||||
// getDatabasePath gets the database file path
|
||||
func (ds *DocumentService) getDatabasePath() (string, error) {
|
||||
config, err := ds.configService.GetConfig()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(config.General.DataPath, "docs", "default.json"), nil
|
||||
return filepath.Join(config.General.DataPath, dbName), nil
|
||||
}
|
||||
|
||||
// loadDocument 加载文档
|
||||
func (ds *DocumentService) loadDocument() {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
doc := ds.docStore.Get()
|
||||
if doc.Meta.ID == "" {
|
||||
ds.document = models.NewDefaultDocument()
|
||||
ds.docStore.Set(*ds.document)
|
||||
} else {
|
||||
ds.document = &doc
|
||||
// createIndexes creates database indexes
|
||||
func (ds *DocumentService) createIndexes() error {
|
||||
indexes := []string{
|
||||
sqlCreateIndexUpdatedAt,
|
||||
sqlCreateIndexTitle,
|
||||
}
|
||||
|
||||
ds.lastSaveTime.Store(time.Now().Unix())
|
||||
}
|
||||
|
||||
// GetActiveDocument 获取活动文档
|
||||
func (ds *DocumentService) GetActiveDocument() (*models.Document, error) {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
if ds.document == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
docCopy := *ds.document
|
||||
return &docCopy, nil
|
||||
}
|
||||
|
||||
// UpdateActiveDocumentContent 更新文档内容
|
||||
func (ds *DocumentService) UpdateActiveDocumentContent(content string) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
if ds.document != nil && ds.document.Content != content {
|
||||
ds.document.Content = content
|
||||
ds.markDirty()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// markDirty 标记为脏数据并触发自动保存
|
||||
func (ds *DocumentService) markDirty() {
|
||||
if ds.isDirty.CompareAndSwap(false, true) {
|
||||
select {
|
||||
case ds.saveScheduler <- struct{}{}:
|
||||
default: // 已有保存任务在队列中
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ForceSave 强制保存
|
||||
func (ds *DocumentService) ForceSave() error {
|
||||
return ds.saveDocument()
|
||||
}
|
||||
|
||||
// saveDocument 保存文档
|
||||
func (ds *DocumentService) saveDocument() error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
if ds.document == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
ds.document.Meta.LastUpdated = now
|
||||
|
||||
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ds.docStore.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds.isDirty.Store(false)
|
||||
ds.lastSaveTime.Store(now.Unix())
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoSaveWorker 自动保存工作协程
|
||||
func (ds *DocumentService) autoSaveWorker() {
|
||||
ticker := time.NewTicker(ds.getAutoSaveInterval())
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ds.ctx.Done():
|
||||
return
|
||||
case <-ds.saveScheduler:
|
||||
ds.performAutoSave()
|
||||
case <-ticker.C:
|
||||
if ds.isDirty.Load() {
|
||||
ds.performAutoSave()
|
||||
}
|
||||
// 动态调整保存间隔
|
||||
ticker.Reset(ds.getAutoSaveInterval())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getAutoSaveInterval 获取自动保存间隔
|
||||
func (ds *DocumentService) getAutoSaveInterval() time.Duration {
|
||||
config, err := ds.configService.GetConfig()
|
||||
if err != nil {
|
||||
return 5 * time.Second
|
||||
}
|
||||
return time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond
|
||||
}
|
||||
|
||||
// performAutoSave 执行自动保存
|
||||
func (ds *DocumentService) performAutoSave() {
|
||||
if !ds.isDirty.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
// 防抖:避免过于频繁的保存
|
||||
lastSave := time.Unix(ds.lastSaveTime.Load(), 0)
|
||||
if time.Since(lastSave) < time.Second {
|
||||
// 延迟重试
|
||||
time.AfterFunc(time.Second, func() {
|
||||
select {
|
||||
case ds.saveScheduler <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := ds.saveDocument(); err != nil {
|
||||
ds.logger.Error("auto save failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadDocument 重新加载文档
|
||||
func (ds *DocumentService) ReloadDocument() error {
|
||||
// 先保存当前文档
|
||||
if ds.isDirty.Load() {
|
||||
if err := ds.saveDocument(); err != nil {
|
||||
for _, index := range indexes {
|
||||
if _, err := ds.db.Exec(index); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 重新初始化存储
|
||||
if err := ds.initStore(); err != nil {
|
||||
// ensureDefaultDocument ensures a default document exists
|
||||
func (ds *DocumentService) ensureDefaultDocument() error {
|
||||
// Check if any document exists
|
||||
var count int
|
||||
err := ds.db.QueryRow("SELECT COUNT(*) FROM documents").Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重新加载
|
||||
ds.loadDocument()
|
||||
// If no documents exist, create default document
|
||||
if count == 0 {
|
||||
defaultDoc := models.NewDefaultDocument()
|
||||
_, err := ds.CreateDocument(defaultDoc.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (ds *DocumentService) ServiceShutdown() error {
|
||||
ds.cancel() // 停止自动保存工作协程
|
||||
// GetDocumentByID gets a document by ID
|
||||
func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
// 最后保存
|
||||
if ds.isDirty.Load() {
|
||||
return ds.saveDocument()
|
||||
var doc models.Document
|
||||
row := ds.db.QueryRow(sqlGetDocumentByID, id)
|
||||
err := row.Scan(&doc.ID, &doc.Title, &doc.Content, &doc.CreatedAt, &doc.UpdatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get document by ID: %w", err)
|
||||
}
|
||||
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// CreateDocument creates a new document and returns the created document with ID
|
||||
func (ds *DocumentService) CreateDocument(title string) (*models.Document, error) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
// Create document with default content
|
||||
now := time.Now()
|
||||
doc := &models.Document{
|
||||
Title: title,
|
||||
Content: "∞∞∞text-a\n",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
result, err := ds.db.Exec(sqlInsertDocument, doc.Title, doc.Content, doc.CreatedAt, doc.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create document: %w", err)
|
||||
}
|
||||
|
||||
// Get the auto-generated ID
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get last insert ID: %w", err)
|
||||
}
|
||||
|
||||
// Return the created document with ID
|
||||
doc.ID = id
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// UpdateDocumentContent updates the content of a document
|
||||
func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
_, err := ds.db.Exec(sqlUpdateDocumentContent, content, time.Now(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update document content: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnDataPathChanged 处理数据路径变更
|
||||
func (ds *DocumentService) OnDataPathChanged(oldPath, newPath string) error {
|
||||
return ds.ReloadDocument()
|
||||
// UpdateDocumentTitle updates the title of a document
|
||||
func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
_, err := ds.db.Exec(sqlUpdateDocumentTitle, title, time.Now(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update document title: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDocument deletes a document (not allowed if it's the only document)
|
||||
func (ds *DocumentService) DeleteDocument(id int64) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
// Check if this is the only document
|
||||
var count int
|
||||
err := ds.db.QueryRow("SELECT COUNT(*) FROM documents").Scan(&count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to count documents: %w", err)
|
||||
}
|
||||
|
||||
// Don't allow deletion if this is the only document
|
||||
if count <= 1 {
|
||||
return fmt.Errorf("cannot delete the last document")
|
||||
}
|
||||
|
||||
_, err = ds.db.Exec(sqlDeleteDocument, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete document: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAllDocumentsMeta lists all document metadata
|
||||
func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
rows, err := ds.db.Query(sqlListAllDocumentsMeta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list document meta: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var documents []*models.Document
|
||||
for rows.Next() {
|
||||
var doc models.Document
|
||||
err := rows.Scan(&doc.ID, &doc.Title, &doc.CreatedAt, &doc.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan document meta: %w", err)
|
||||
}
|
||||
documents = append(documents, &doc)
|
||||
}
|
||||
|
||||
return documents, nil
|
||||
}
|
||||
|
||||
// GetFirstDocumentID gets the first document's ID for frontend initialization
|
||||
func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
var id int64
|
||||
err := ds.db.QueryRow(sqlGetFirstDocumentID).Scan(&id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil // No documents exist
|
||||
}
|
||||
return 0, fmt.Errorf("failed to get first document ID: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// OnShutdown shuts down the service when the application closes
|
||||
func (ds *DocumentService) OnShutdown() error {
|
||||
if ds.db != nil {
|
||||
return ds.db.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnDataPathChanged handles data path changes
|
||||
func (ds *DocumentService) OnDataPathChanged() error {
|
||||
// Close existing database
|
||||
if ds.db != nil {
|
||||
ds.db.Close()
|
||||
}
|
||||
|
||||
// Reinitialize with new path
|
||||
return ds.initDatabase()
|
||||
}
|
||||
|
@@ -257,8 +257,8 @@ func (es *ExtensionService) ResetAllExtensionsToDefault() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (es *ExtensionService) ServiceShutdown() error {
|
||||
// OnShutdown 关闭服务
|
||||
func (es *ExtensionService) OnShutdown() error {
|
||||
es.cancel()
|
||||
return nil
|
||||
}
|
||||
|
@@ -259,7 +259,7 @@ func (hs *HotkeyService) IsRegistered() bool {
|
||||
return hs.isRegistered.Load()
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
// OnShutdown 关闭服务
|
||||
func (hs *HotkeyService) ServiceShutdown() error {
|
||||
hs.cancel()
|
||||
hs.wg.Wait()
|
||||
|
@@ -283,8 +283,8 @@ func (hs *HotkeyService) IsRegistered() bool {
|
||||
return hs.isRegistered.Load()
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭热键服务
|
||||
func (hs *HotkeyService) ServiceShutdown() error {
|
||||
// OnShutdown 关闭热键服务
|
||||
func (hs *HotkeyService) OnShutdown() error {
|
||||
return hs.UnregisterHotkey()
|
||||
}
|
||||
|
||||
|
@@ -384,8 +384,8 @@ func (hs *HotkeyService) IsRegistered() bool {
|
||||
return hs.isRegistered.Load()
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (hs *HotkeyService) ServiceShutdown() error {
|
||||
// OnShutdown 关闭服务
|
||||
func (hs *HotkeyService) OnShutdown() error {
|
||||
hs.cancel()
|
||||
hs.wg.Wait()
|
||||
C.closeX11Display()
|
||||
|
@@ -185,8 +185,8 @@ func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) {
|
||||
return config.KeyBindings, nil
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (kbs *KeyBindingService) ServiceShutdown() error {
|
||||
// OnShutdown 关闭服务
|
||||
func (kbs *KeyBindingService) OnShutdown() error {
|
||||
kbs.cancel()
|
||||
return nil
|
||||
}
|
||||
|
@@ -417,8 +417,8 @@ func (ms *MigrationService) CancelMigration() error {
|
||||
return fmt.Errorf("no active migration to cancel")
|
||||
}
|
||||
|
||||
// ServiceShutdown 服务关闭
|
||||
func (ms *MigrationService) ServiceShutdown() error {
|
||||
// OnShutdown 服务关闭
|
||||
func (ms *MigrationService) OnShutdown() error {
|
||||
ms.CancelMigration()
|
||||
return nil
|
||||
}
|
||||
|
@@ -74,19 +74,13 @@ func NewServiceManager() *ServiceManager {
|
||||
}
|
||||
|
||||
// 设置数据路径变更监听,处理配置重置和路径变更
|
||||
err = configService.SetDataPathChangeCallback(func(oldPath, newPath string) error {
|
||||
return documentService.OnDataPathChanged(oldPath, newPath)
|
||||
err = configService.SetDataPathChangeCallback(func() error {
|
||||
return documentService.OnDataPathChanged()
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 初始化文档服务
|
||||
err = documentService.Initialize()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &ServiceManager{
|
||||
configService: configService,
|
||||
documentService: documentService,
|
||||
|
Reference in New Issue
Block a user