diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index ebc65d4..93cf158 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -33,6 +33,11 @@ export class AppConfig { */ "updates": UpdatesConfig; + /** + * Git同步设置 + */ + "sync": GitSyncConfig; + /** * 配置元数据 */ @@ -52,6 +57,9 @@ export class AppConfig { if (!("updates" in $$source)) { this["updates"] = (new UpdatesConfig()); } + if (!("sync" in $$source)) { + this["sync"] = (new GitSyncConfig()); + } if (!("metadata" in $$source)) { this["metadata"] = (new ConfigMetadata()); } @@ -68,6 +76,7 @@ export class AppConfig { const $$createField2_0 = $$createType2; const $$createField3_0 = $$createType3; const $$createField4_0 = $$createType4; + const $$createField5_0 = $$createType5; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("general" in $$parsedSource) { $$parsedSource["general"] = $$createField0_0($$parsedSource["general"]); @@ -81,8 +90,11 @@ export class AppConfig { if ("updates" in $$parsedSource) { $$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]); } + if ("sync" in $$parsedSource) { + $$parsedSource["sync"] = $$createField4_0($$parsedSource["sync"]); + } if ("metadata" in $$parsedSource) { - $$parsedSource["metadata"] = $$createField4_0($$parsedSource["metadata"]); + $$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]); } return new AppConfig($$parsedSource as Partial); } @@ -126,7 +138,7 @@ export class AppearanceConfig { * Creates a new AppearanceConfig instance from a string or object. */ static createFrom($$source: any = {}): AppearanceConfig { - const $$createField2_0 = $$createType5; + const $$createField2_0 = $$createType6; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("customTheme" in $$parsedSource) { $$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]); @@ -135,6 +147,31 @@ export class AppearanceConfig { } } +/** + * AuthMethod 定义Git认证方式 + */ +export enum AuthMethod { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * 个人访问令牌 + */ + Token = "token", + + /** + * SSH密钥 + */ + SSHKey = "ssh_key", + + /** + * 用户名密码 + */ + UserPass = "user_pass", +}; + /** * ConfigMetadata 配置元数据 */ @@ -200,8 +237,8 @@ export class CustomThemeConfig { * Creates a new CustomThemeConfig instance from a string or object. */ static createFrom($$source: any = {}): CustomThemeConfig { - const $$createField0_0 = $$createType6; - const $$createField1_0 = $$createType6; + const $$createField0_0 = $$createType7; + const $$createField1_0 = $$createType7; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("darkTheme" in $$parsedSource) { $$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]); @@ -224,6 +261,11 @@ export class Document { "updatedAt": time$0.Time; "is_deleted": boolean; + /** + * 锁定标志,锁定的文档无法被删除 + */ + "is_locked": boolean; + /** Creates a new Document instance. */ constructor($$source: Partial = {}) { if (!("id" in $$source)) { @@ -244,6 +286,9 @@ export class Document { if (!("is_deleted" in $$source)) { this["is_deleted"] = false; } + if (!("is_locked" in $$source)) { + this["is_locked"] = false; + } Object.assign(this, $$source); } @@ -389,7 +434,7 @@ export class Extension { * Creates a new Extension instance from a string or object. */ static createFrom($$source: any = {}): Extension { - const $$createField3_0 = $$createType7; + const $$createField3_0 = $$createType8; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("config" in $$parsedSource) { $$parsedSource["config"] = $$createField3_0($$parsedSource["config"]); @@ -522,7 +567,7 @@ export class GeneralConfig { * Creates a new GeneralConfig instance from a string or object. */ static createFrom($$source: any = {}): GeneralConfig { - const $$createField5_0 = $$createType9; + const $$createField5_0 = $$createType10; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("globalHotkey" in $$parsedSource) { $$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); @@ -531,6 +576,91 @@ export class GeneralConfig { } } +/** + * GitSyncConfig 保存Git同步的配置信息 + */ +export class GitSyncConfig { + "enabled": boolean; + "repo_url": string; + "branch": string; + "auth_method": AuthMethod; + "username"?: string; + "password"?: string; + "token"?: string; + "ssh_key_path"?: string; + "ssh_key_passphrase"?: string; + + /** + * 同步间隔(分钟) + */ + "sync_interval": number; + "last_sync_time": time$0.Time; + + /** + * 是否启用自动同步 + */ + "auto_sync": boolean; + "local_repo_path": string; + + /** + * 合并冲突策略 + */ + "sync_strategy": SyncStrategy; + + /** + * 要同步的文件列表,默认为数据库文件 + */ + "files_to_sync": string[]; + + /** Creates a new GitSyncConfig instance. */ + constructor($$source: Partial = {}) { + if (!("enabled" in $$source)) { + this["enabled"] = false; + } + if (!("repo_url" in $$source)) { + this["repo_url"] = ""; + } + if (!("branch" in $$source)) { + this["branch"] = ""; + } + if (!("auth_method" in $$source)) { + this["auth_method"] = ("" as AuthMethod); + } + if (!("sync_interval" in $$source)) { + this["sync_interval"] = 0; + } + if (!("last_sync_time" in $$source)) { + this["last_sync_time"] = null; + } + if (!("auto_sync" in $$source)) { + this["auto_sync"] = false; + } + if (!("local_repo_path" in $$source)) { + this["local_repo_path"] = ""; + } + if (!("sync_strategy" in $$source)) { + this["sync_strategy"] = ("" as SyncStrategy); + } + if (!("files_to_sync" in $$source)) { + this["files_to_sync"] = []; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new GitSyncConfig instance from a string or object. + */ + static createFrom($$source: any = {}): GitSyncConfig { + const $$createField14_0 = $$createType11; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("files_to_sync" in $$parsedSource) { + $$parsedSource["files_to_sync"] = $$createField14_0($$parsedSource["files_to_sync"]); + } + return new GitSyncConfig($$parsedSource as Partial); + } +} + /** * GiteaConfig Gitea配置 */ @@ -1038,6 +1168,26 @@ export enum LanguageType { LangEnUS = "en-US", }; +/** + * SyncStrategy 定义同步策略 + */ +export enum SyncStrategy { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * LocalFirst 本地优先:如有冲突,保留本地修改 + */ + LocalFirst = "local_first", + + /** + * RemoteFirst 远程优先:如有冲突,采用远程版本 + */ + RemoteFirst = "remote_first", +}; + /** * SystemThemeType 系统主题类型定义 */ @@ -1389,8 +1539,8 @@ export class UpdatesConfig { * Creates a new UpdatesConfig instance from a string or object. */ static createFrom($$source: any = {}): UpdatesConfig { - const $$createField6_0 = $$createType10; - const $$createField7_0 = $$createType11; + const $$createField6_0 = $$createType12; + const $$createField7_0 = $$createType13; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("github" in $$parsedSource) { $$parsedSource["github"] = $$createField6_0($$parsedSource["github"]); @@ -1407,16 +1557,18 @@ const $$createType0 = GeneralConfig.createFrom; const $$createType1 = EditingConfig.createFrom; const $$createType2 = AppearanceConfig.createFrom; const $$createType3 = UpdatesConfig.createFrom; -const $$createType4 = ConfigMetadata.createFrom; -const $$createType5 = CustomThemeConfig.createFrom; -const $$createType6 = ThemeColorConfig.createFrom; -var $$createType7 = (function $$initCreateType7(...args): any { - if ($$createType7 === $$initCreateType7) { - $$createType7 = $$createType8; +const $$createType4 = GitSyncConfig.createFrom; +const $$createType5 = ConfigMetadata.createFrom; +const $$createType6 = CustomThemeConfig.createFrom; +const $$createType7 = ThemeColorConfig.createFrom; +var $$createType8 = (function $$initCreateType8(...args): any { + if ($$createType8 === $$initCreateType8) { + $$createType8 = $$createType9; } - return $$createType7(...args); + return $$createType8(...args); }); -const $$createType8 = $Create.Map($Create.Any, $Create.Any); -const $$createType9 = HotkeyCombo.createFrom; -const $$createType10 = GithubConfig.createFrom; -const $$createType11 = GiteaConfig.createFrom; +const $$createType9 = $Create.Map($Create.Any, $Create.Any); +const $$createType10 = HotkeyCombo.createFrom; +const $$createType11 = $Create.Array($Create.Any); +const $$createType12 = GithubConfig.createFrom; +const $$createType13 = GiteaConfig.createFrom; diff --git a/frontend/bindings/voidraft/internal/services/databaseservice.ts b/frontend/bindings/voidraft/internal/services/databaseservice.ts index 0e86663..77f13cc 100644 --- a/frontend/bindings/voidraft/internal/services/databaseservice.ts +++ b/frontend/bindings/voidraft/internal/services/databaseservice.ts @@ -22,6 +22,14 @@ export function OnDataPathChanged(): Promise & { cancel(): void } { return $resultPromise; } +/** + * RegisterModel 注册模型与表的映射关系 + */ +export function RegisterModel(tableName: string, model: any): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(175397515, tableName, model) as any; + return $resultPromise; +} + /** * ServiceShutdown shuts down the service when the application closes */ diff --git a/frontend/bindings/voidraft/internal/services/documentservice.ts b/frontend/bindings/voidraft/internal/services/documentservice.ts index 6a96298..c630d60 100644 --- a/frontend/bindings/voidraft/internal/services/documentservice.ts +++ b/frontend/bindings/voidraft/internal/services/documentservice.ts @@ -81,6 +81,14 @@ export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[] return $typingPromise; } +/** + * LockDocument 锁定文档,防止删除 + */ +export function LockDocument(id: number): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1889494473, id) as any; + return $resultPromise; +} + /** * RestoreDocument restores a deleted document */ @@ -97,6 +105,14 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(222307930, id) as any; + return $resultPromise; +} + /** * UpdateDocumentContent updates the content of a document */ diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index 261633c..a5afaf2 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -11,6 +11,9 @@ import { TabType, UpdatesConfig, UpdateSourceType, + GitSyncConfig, + AuthMethod, + SyncStrategy } from '@/../bindings/voidraft/internal/models/models'; import {useI18n} from 'vue-i18n'; import {ConfigUtils} from '@/utils/configUtils'; @@ -48,6 +51,10 @@ type UpdatesConfigKeyMap = { readonly [K in keyof UpdatesConfig]: string; }; +type SyncConfigKeyMap = { + readonly [K in keyof GitSyncConfig]: string; +}; + type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight'; // 配置键映射 @@ -88,6 +95,24 @@ const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = { gitea: 'updates.gitea' } as const; +const SYNC_CONFIG_KEY_MAP: SyncConfigKeyMap = { + enabled: 'sync.enabled', + repo_url: 'sync.repo_url', + branch: 'sync.branch', + auth_method: 'sync.auth_method', + username: 'sync.username', + password: 'sync.password', + token: 'sync.token', + ssh_key_path: 'sync.ssh_key_path', + ssh_key_passphrase: 'sync.ssh_key_passphrase', + sync_interval: 'sync.sync_interval', + last_sync_time: 'sync.last_sync_time', + auto_sync: 'sync.auto_sync', + local_repo_path: 'sync.local_repo_path', + sync_strategy: 'sync.sync_strategy', + files_to_sync: 'sync.files_to_sync' +} as const; + // 配置限制 const CONFIG_LIMITS = { fontSize: {min: 12, max: 28, default: 13}, @@ -261,6 +286,23 @@ const DEFAULT_CONFIG: AppConfig = { repo: "voidraft", } }, + sync: { + enabled: false, + repo_url: "", + branch: "main", + auth_method: AuthMethod.Token, + username: "", + password: "", + token: "", + ssh_key_path: "", + ssh_key_passphrase: "", + sync_interval: 60, + last_sync_time: null, + auto_sync: true, + local_repo_path: "", + sync_strategy: SyncStrategy.LocalFirst, + files_to_sync: ["voidraft.db"] + }, metadata: { version: '1.0.0', lastUpdated: new Date().toString(), @@ -348,6 +390,21 @@ export const useConfigStore = defineStore('config', () => { state.config.updates[key] = value; }; + const updateSyncConfig = async (key: K, value: GitSyncConfig[K]): Promise => { + // 确保配置已加载 + if (!state.configLoaded && !state.isLoading) { + await initConfig(); + } + + const backendKey = SYNC_CONFIG_KEY_MAP[key]; + if (!backendKey) { + throw new Error(`No backend key mapping found for sync.${key.toString()}`); + } + + await ConfigService.Set(backendKey, value); + state.config.sync[key] = value; + }; + // 加载配置 const initConfig = async (): Promise => { if (state.isLoading) return; @@ -362,6 +419,7 @@ export const useConfigStore = defineStore('config', () => { if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing); if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance); if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates); + if (appConfig.sync) Object.assign(state.config.sync, appConfig.sync); if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata); } @@ -594,6 +652,20 @@ export const useConfigStore = defineStore('config', () => { }, // 更新配置相关方法 - setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value) + setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value), + + // Git同步配置相关方法 + setGitSyncEnabled: (value: boolean) => updateSyncConfig('enabled', value), + setGitRepoUrl: (value: string) => updateSyncConfig('repo_url', value), + setGitBranch: (value: string) => updateSyncConfig('branch', value), + setAuthMethod: (value: AuthMethod) => updateSyncConfig('auth_method', value), + setGitUsername: (value: string) => updateSyncConfig('username', value), + setGitPassword: (value: string) => updateSyncConfig('password', value), + setGitToken: (value: string) => updateSyncConfig('token', value), + setSSHKeyPath: (value: string) => updateSyncConfig('ssh_key_path', value), + setSyncInterval: (value: number) => updateSyncConfig('sync_interval', value), + setAutoSync: (value: boolean) => updateSyncConfig('auto_sync', value), + setSyncStrategy: (value: SyncStrategy) => updateSyncConfig('sync_strategy', value), + setFilesToSync: (value: string[]) => updateSyncConfig('files_to_sync', value), }; }); \ No newline at end of file diff --git a/internal/models/config.go b/internal/models/config.go index 93dbf05..7cfeb4c 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -124,6 +124,7 @@ type AppConfig struct { Editing EditingConfig `json:"editing"` // 编辑设置 Appearance AppearanceConfig `json:"appearance"` // 外观设置 Updates UpdatesConfig `json:"updates"` // 更新设置 + Sync GitSyncConfig `json:"sync"` // Git同步设置 Metadata ConfigMetadata `json:"metadata"` // 配置元数据 } @@ -138,6 +139,7 @@ func NewDefaultAppConfig() *AppConfig { currentDir, _ := os.UserHomeDir() dataDir := filepath.Join(currentDir, ".voidraft", "data") + repoPath := filepath.Join(currentDir, ".voidraft", "sync-repo") return &AppConfig{ General: GeneralConfig{ @@ -173,7 +175,7 @@ func NewDefaultAppConfig() *AppConfig { CustomTheme: *NewDefaultCustomThemeConfig(), }, Updates: UpdatesConfig{ - Version: "1.0.0", + Version: "1.2.0", AutoUpdate: true, PrimarySource: UpdateSourceGithub, BackupSource: UpdateSourceGitea, @@ -189,9 +191,25 @@ func NewDefaultAppConfig() *AppConfig { Repo: "voidraft", }, }, + Sync: GitSyncConfig{ + Enabled: false, + RepoURL: "", + Branch: "main", + AuthMethod: Token, + Username: "", + Password: "", + Token: "", + SSHKeyPath: "", + SyncInterval: 60, + LastSyncTime: time.Time{}, + AutoSync: true, + LocalRepoPath: repoPath, + SyncStrategy: LocalFirst, + FilesToSync: []string{"voidraft.db"}, + }, Metadata: ConfigMetadata{ LastUpdated: time.Now().Format(time.RFC3339), - Version: "1.0.0", + Version: "1.2.0", }, } } diff --git a/internal/models/document.go b/internal/models/document.go index ca26583..9b7b71f 100644 --- a/internal/models/document.go +++ b/internal/models/document.go @@ -12,9 +12,10 @@ type Document struct { CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` IsDeleted bool `json:"is_deleted"` + IsLocked bool `json:"is_locked" db:"is_locked"` // 锁定标志,锁定的文档无法被删除 } -// NewDocument 创建新文档(不需要传ID,由数据库自增) +// NewDocument 创建新文档 func NewDocument(title, content string) *Document { now := time.Now() return &Document{ @@ -23,6 +24,7 @@ func NewDocument(title, content string) *Document { CreatedAt: now, UpdatedAt: now, IsDeleted: false, + IsLocked: false, // 默认不锁定 } } diff --git a/internal/models/extensions.go b/internal/models/extensions.go index ce73eb0..436631c 100644 --- a/internal/models/extensions.go +++ b/internal/models/extensions.go @@ -4,10 +4,10 @@ import "time" // Extension 单个扩展配置 type Extension struct { - ID ExtensionID `json:"id"` // 扩展唯一标识 - Enabled bool `json:"enabled"` // 是否启用 - IsDefault bool `json:"isDefault"` // 是否为默认扩展 - Config ExtensionConfig `json:"config"` // 扩展配置项 + ID ExtensionID `json:"id" db:"id"` // 扩展唯一标识 + Enabled bool `json:"enabled" db:"enabled"` // 是否启用 + IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认扩展 + Config ExtensionConfig `json:"config" db:"config"` // 扩展配置项 } // ExtensionID 扩展标识符 diff --git a/internal/models/key_bindings.go b/internal/models/key_bindings.go index ce313f5..50c297c 100644 --- a/internal/models/key_bindings.go +++ b/internal/models/key_bindings.go @@ -4,11 +4,11 @@ import "time" // KeyBinding 单个快捷键绑定 type KeyBinding struct { - Command KeyBindingCommand `json:"command"` // 快捷键动作 - Extension ExtensionID `json:"extension"` // 所属扩展 - Key string `json:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p") - Enabled bool `json:"enabled"` // 是否启用 - IsDefault bool `json:"isDefault"` // 是否为默认快捷键 + Command KeyBindingCommand `json:"command" db:"command"` // 快捷键动作 + Extension ExtensionID `json:"extension" db:"extension"` // 所属扩展 + Key string `json:"key" db:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p") + Enabled bool `json:"enabled" db:"enabled"` // 是否启用 + IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认快捷键 } // KeyBindingCommand 快捷键命令 diff --git a/internal/models/sync.go b/internal/models/sync.go new file mode 100644 index 0000000..f3f6d01 --- /dev/null +++ b/internal/models/sync.go @@ -0,0 +1,63 @@ +package models + +import "time" + +// AuthMethod 定义Git认证方式 +type AuthMethod string + +const ( + Token AuthMethod = "token" // 个人访问令牌 + SSHKey AuthMethod = "ssh_key" // SSH密钥 + UserPass AuthMethod = "user_pass" // 用户名密码 +) + +// SyncStrategy 定义同步策略 +type SyncStrategy string + +const ( + // LocalFirst 本地优先:如有冲突,保留本地修改 + LocalFirst SyncStrategy = "local_first" + // RemoteFirst 远程优先:如有冲突,采用远程版本 + RemoteFirst SyncStrategy = "remote_first" +) + +// GitSyncConfig 保存Git同步的配置信息 +type GitSyncConfig struct { + Enabled bool `json:"enabled"` + RepoURL string `json:"repo_url"` + Branch string `json:"branch"` + AuthMethod AuthMethod `json:"auth_method"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + SSHKeyPath string `json:"ssh_key_path,omitempty"` + SSHKeyPassphrase string `json:"ssh_key_passphrase,omitempty"` + SyncInterval int `json:"sync_interval"` // 同步间隔(分钟) + LastSyncTime time.Time `json:"last_sync_time"` + AutoSync bool `json:"auto_sync"` // 是否启用自动同步 + LocalRepoPath string `json:"local_repo_path"` + SyncStrategy SyncStrategy `json:"sync_strategy"` // 合并冲突策略 + FilesToSync []string `json:"files_to_sync"` // 要同步的文件列表,默认为数据库文件 +} + +// GitSyncStatus 保存同步状态信息 +type GitSyncStatus struct { + IsSyncing bool `json:"is_syncing"` + LastSyncTime time.Time `json:"last_sync_time"` + LastSyncStatus string `json:"last_sync_status"` // success, failed, conflict + LastErrorMsg string `json:"last_error_msg,omitempty"` + LastCommitID string `json:"last_commit_id,omitempty"` + RemoteCommitID string `json:"remote_commit_id,omitempty"` + CommitAhead int `json:"commit_ahead"` // 本地领先远程的提交数 + CommitBehind int `json:"commit_behind"` // 本地落后远程的提交数 +} + +// SyncLogEntry 记录每次同步操作的日志 +type SyncLogEntry struct { + ID int64 `json:"id"` + Timestamp time.Time `json:"timestamp"` + Action string `json:"action"` // push, pull, reset + Status string `json:"status"` // success, failed + Message string `json:"message,omitempty"` + ChangedFiles int `json:"changed_files"` +} diff --git a/internal/services/database_service.go b/internal/services/database_service.go index b88f4d2..4155eb4 100644 --- a/internal/services/database_service.go +++ b/internal/services/database_service.go @@ -5,7 +5,11 @@ import ( "fmt" "os" "path/filepath" + "reflect" + "strings" "sync" + "time" + "voidraft/internal/models" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/log" @@ -31,7 +35,8 @@ CREATE TABLE IF NOT EXISTS documents ( content TEXT DEFAULT '∞∞∞text-a', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - is_deleted INTEGER DEFAULT 0 + is_deleted INTEGER DEFAULT 0, + is_locked INTEGER DEFAULT 0 )` // Extensions table @@ -58,8 +63,34 @@ CREATE TABLE IF NOT EXISTS key_bindings ( updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(command, extension) )` + + // Git sync logs table + sqlCreateSyncLogsTable = ` +CREATE TABLE IF NOT EXISTS sync_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + action TEXT NOT NULL, + status TEXT NOT NULL, + message TEXT, + commit_id TEXT, + changed_files INTEGER DEFAULT 0, + repo_url TEXT NOT NULL, + branch TEXT NOT NULL +)` ) +// ColumnInfo 存储列的信息 +type ColumnInfo struct { + SQLType string + DefaultValue string +} + +// TableModel 表示表与模型之间的映射关系 +type TableModel struct { + TableName string + Model interface{} +} + // DatabaseService provides shared database functionality type DatabaseService struct { configService *ConfigService @@ -67,6 +98,7 @@ type DatabaseService struct { SQLite *sqlite.Service mu sync.RWMutex ctx context.Context + tableModels []TableModel // 注册的表模型 } // NewDatabaseService creates a new database service @@ -75,11 +107,28 @@ func NewDatabaseService(configService *ConfigService, logger *log.Service) *Data logger = log.New() } - return &DatabaseService{ + ds := &DatabaseService{ configService: configService, logger: logger, SQLite: sqlite.New(), } + + // 注册所有模型 + ds.registerAllModels() + + return ds +} + +// registerAllModels 注册所有数据模型,集中管理表-模型映射 +func (ds *DatabaseService) registerAllModels() { + // 文档表 + ds.RegisterModel("documents", &models.Document{}) + // 扩展表 + ds.RegisterModel("extensions", &models.Extension{}) + // 快捷键表 + ds.RegisterModel("key_bindings", &models.KeyBinding{}) + // 同步日志表 + ds.RegisterModel("sync_logs", &models.SyncLogEntry{}) } // ServiceStartup initializes the service when the application starts @@ -135,6 +184,11 @@ func (ds *DatabaseService) initDatabase() error { return fmt.Errorf("failed to create indexes: %w", err) } + // 执行模型与表结构同步 + if err := ds.syncAllModelTables(); err != nil { + return fmt.Errorf("failed to sync model tables: %w", err) + } + return nil } @@ -153,6 +207,7 @@ func (ds *DatabaseService) createTables() error { sqlCreateDocumentsTable, sqlCreateExtensionsTable, sqlCreateKeyBindingsTable, + sqlCreateSyncLogsTable, } for _, table := range tables { @@ -176,6 +231,10 @@ func (ds *DatabaseService) createIndexes() error { `CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`, + // Sync logs indexes + `CREATE INDEX IF NOT EXISTS idx_sync_logs_timestamp ON sync_logs(timestamp DESC)`, + `CREATE INDEX IF NOT EXISTS idx_sync_logs_action ON sync_logs(action)`, + `CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status)`, } for _, index := range indexes { @@ -186,6 +245,149 @@ func (ds *DatabaseService) createIndexes() error { return nil } +// RegisterModel 注册模型与表的映射关系 +func (ds *DatabaseService) RegisterModel(tableName string, model interface{}) { + ds.mu.Lock() + defer ds.mu.Unlock() + + ds.tableModels = append(ds.tableModels, TableModel{ + TableName: tableName, + Model: model, + }) +} + +// syncAllModelTables 同步所有注册的模型与表结构 +func (ds *DatabaseService) syncAllModelTables() error { + for _, tm := range ds.tableModels { + if err := ds.syncModelTable(tm.TableName, tm.Model); err != nil { + return fmt.Errorf("failed to sync table %s: %w", tm.TableName, err) + } + } + return nil +} + +// syncModelTable 同步模型与表结构 +func (ds *DatabaseService) syncModelTable(tableName string, model interface{}) error { + // 获取表结构元数据 + columns, err := ds.getTableColumns(tableName) + if err != nil { + return fmt.Errorf("failed to get table columns: %w", err) + } + + // 使用反射从模型中提取字段信息 + expectedColumns, err := ds.getModelColumns(model) + if err != nil { + return fmt.Errorf("failed to get model columns: %w", err) + } + + // 检查缺失的列并添加 + for colName, colInfo := range expectedColumns { + if _, exists := columns[colName]; !exists { + // 执行添加列的SQL + alterSQL := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s DEFAULT %s", + tableName, colName, colInfo.SQLType, colInfo.DefaultValue) + if err := ds.SQLite.Execute(alterSQL); err != nil { + return fmt.Errorf("failed to add column %s: %w", colName, err) + } + } + } + + return nil +} + +// getTableColumns 获取表的列信息 +func (ds *DatabaseService) getTableColumns(table string) (map[string]string, error) { + query := fmt.Sprintf("PRAGMA table_info(%s)", table) + rows, err := ds.SQLite.Query(query) + if err != nil { + return nil, err + } + + columns := make(map[string]string) + for _, row := range rows { + name, ok1 := row["name"].(string) + typeName, ok2 := row["type"].(string) + + if !ok1 || !ok2 { + continue + } + + columns[name] = typeName + } + + return columns, nil +} + +// getModelColumns 从模型结构体中提取数据库列信息 +func (ds *DatabaseService) getModelColumns(model interface{}) (map[string]ColumnInfo, error) { + columns := make(map[string]ColumnInfo) + + // 使用反射获取结构体的类型信息 + t := reflect.TypeOf(model) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("model must be a struct or a pointer to struct") + } + + // 遍历所有字段 + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + // 获取数据库字段名 + dbTag := field.Tag.Get("db") + if dbTag == "" { + // 如果没有db标签,则使用字段名的蛇形命名方式 + dbTag = toSnakeCase(field.Name) + } + + // 获取字段类型对应的SQL类型和默认值 + sqlType, defaultVal := getSQLTypeAndDefault(field.Type) + + columns[dbTag] = ColumnInfo{ + SQLType: sqlType, + DefaultValue: defaultVal, + } + } + + return columns, nil +} + +// toSnakeCase 将驼峰命名转换为蛇形命名 +func toSnakeCase(s string) string { + var result strings.Builder + for i, r := range s { + if i > 0 && 'A' <= r && r <= 'Z' { + result.WriteRune('_') + } + result.WriteRune(r) + } + return strings.ToLower(result.String()) +} + +// getSQLTypeAndDefault 根据Go类型获取对应的SQL类型和默认值 +func getSQLTypeAndDefault(t reflect.Type) (string, string) { + switch t.Kind() { + case reflect.Bool: + return "INTEGER", "0" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "INTEGER", "0" + case reflect.Float32, reflect.Float64: + return "REAL", "0.0" + case reflect.String: + return "TEXT", "''" + default: + // 处理特殊类型 + if t == reflect.TypeOf(time.Time{}) { + return "DATETIME", "CURRENT_TIMESTAMP" + } + return "TEXT", "NULL" + } +} + // ServiceShutdown shuts down the service when the application closes func (ds *DatabaseService) ServiceShutdown() error { return ds.SQLite.Close() diff --git a/internal/services/document_service.go b/internal/services/document_service.go index 0cd5656..16f9c63 100644 --- a/internal/services/document_service.go +++ b/internal/services/document_service.go @@ -18,28 +18,28 @@ const ( // Document operations sqlGetDocumentByID = ` -SELECT id, title, content, created_at, updated_at, is_deleted +SELECT id, title, content, created_at, updated_at, is_deleted, is_locked FROM documents WHERE id = ?` sqlInsertDocument = ` -INSERT INTO documents (title, content, created_at, updated_at, is_deleted) -VALUES (?, ?, ?, ?, 0)` +INSERT INTO documents (title, content, created_at, updated_at, is_deleted, is_locked) +VALUES (?, ?, ?, ?, 0, 0)` sqlUpdateDocumentContent = ` UPDATE documents SET content = ?, updated_at = ? -WHERE id = ?` +WHERE id = ? AND is_deleted = 0` sqlUpdateDocumentTitle = ` UPDATE documents SET title = ?, updated_at = ? -WHERE id = ?` +WHERE id = ? AND is_deleted = 0` sqlMarkDocumentAsDeleted = ` UPDATE documents SET is_deleted = 1, updated_at = ? -WHERE id = ?` +WHERE id = ? AND is_locked = 0` sqlRestoreDocument = ` UPDATE documents @@ -47,13 +47,13 @@ SET is_deleted = 0, updated_at = ? WHERE id = ?` sqlListAllDocumentsMeta = ` -SELECT id, title, created_at, updated_at +SELECT id, title, created_at, updated_at, is_locked FROM documents WHERE is_deleted = 0 ORDER BY updated_at DESC` sqlListDeletedDocumentsMeta = ` -SELECT id, title, created_at, updated_at +SELECT id, title, created_at, updated_at, is_locked FROM documents WHERE is_deleted = 1 ORDER BY updated_at DESC` @@ -63,6 +63,16 @@ SELECT id FROM documents WHERE is_deleted = 0 ORDER BY id LIMIT 1` sqlCountDocuments = `SELECT COUNT(*) FROM documents WHERE is_deleted = 0` + sqlSetDocumentLocked = ` +UPDATE documents +SET is_locked = 1, updated_at = ? +WHERE id = ?` + + sqlSetDocumentUnlocked = ` +UPDATE documents +SET is_locked = 0, updated_at = ? +WHERE id = ?` + sqlDefaultDocumentID = 1 // 默认文档的ID ) @@ -80,16 +90,19 @@ func NewDocumentService(databaseService *DatabaseService, logger *log.Service) * logger = log.New() } - return &DocumentService{ + ds := &DocumentService{ databaseService: databaseService, logger: logger, } + + return ds } // ServiceStartup initializes the service when the application starts func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { ds.ctx = ctx - // Ensure default document exists + + // 确保默认文档存在 if err := ds.ensureDefaultDocument(); err != nil { return fmt.Errorf("failed to ensure default document: %w", err) } @@ -170,6 +183,10 @@ func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) { doc.IsDeleted = isDeletedInt == 1 } + if isLockedInt, ok := row["is_locked"].(int64); ok { + doc.IsLocked = isLockedInt == 1 + } + return doc, nil } @@ -186,6 +203,7 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error CreatedAt: now, UpdatedAt: now, IsDeleted: false, + IsLocked: false, } // 执行插入操作 @@ -215,6 +233,61 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error return doc, nil } +// LockDocument 锁定文档,防止删除 +func (ds *DocumentService) LockDocument(id int64) error { + ds.mu.Lock() + defer ds.mu.Unlock() + + // 检查文档是否存在且未删除 + doc, err := ds.GetDocumentByID(id) + if err != nil { + return fmt.Errorf("failed to get document: %w", err) + } + if doc == nil { + return fmt.Errorf("document not found: %d", id) + } + if doc.IsDeleted { + return fmt.Errorf("cannot lock deleted document: %d", id) + } + + // 如果已经锁定,无需操作 + if doc.IsLocked { + return nil + } + + err = ds.databaseService.SQLite.Execute(sqlSetDocumentLocked, time.Now(), id) + if err != nil { + return fmt.Errorf("failed to lock document: %w", err) + } + return nil +} + +// UnlockDocument 解锁文档 +func (ds *DocumentService) UnlockDocument(id int64) error { + ds.mu.Lock() + defer ds.mu.Unlock() + + // 检查文档是否存在 + doc, err := ds.GetDocumentByID(id) + if err != nil { + return fmt.Errorf("failed to get document: %w", err) + } + if doc == nil { + return fmt.Errorf("document not found: %d", id) + } + + // 如果未锁定,无需操作 + if !doc.IsLocked { + return nil + } + + err = ds.databaseService.SQLite.Execute(sqlSetDocumentUnlocked, time.Now(), id) + if err != nil { + return fmt.Errorf("failed to unlock document: %w", err) + } + return nil +} + // UpdateDocumentContent updates the content of a document func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error { ds.mu.Lock() @@ -249,7 +322,19 @@ func (ds *DocumentService) DeleteDocument(id int64) error { return fmt.Errorf("cannot delete the default document") } - err := ds.databaseService.SQLite.Execute(sqlMarkDocumentAsDeleted, time.Now(), id) + // 检查文档是否锁定 + doc, err := ds.GetDocumentByID(id) + if err != nil { + return fmt.Errorf("failed to get document: %w", err) + } + if doc == nil { + return fmt.Errorf("document not found: %d", id) + } + if doc.IsLocked { + return fmt.Errorf("cannot delete locked document: %d", id) + } + + err = ds.databaseService.SQLite.Execute(sqlMarkDocumentAsDeleted, time.Now(), id) if err != nil { return fmt.Errorf("failed to mark document as deleted: %w", err) } @@ -304,6 +389,10 @@ func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) { } } + if isLockedInt, ok := row["is_locked"].(int64); ok { + doc.IsLocked = isLockedInt == 1 + } + documents = append(documents, doc) } @@ -346,6 +435,10 @@ func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error } } + if isLockedInt, ok := row["is_locked"].(int64); ok { + doc.IsLocked = isLockedInt == 1 + } + documents = append(documents, doc) }