🎨 Optimize storage logic

This commit is contained in:
2025-07-13 22:32:58 +08:00
parent 6d8fdf62f1
commit b4b0ad9bba
11 changed files with 671 additions and 45 deletions

View File

@@ -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<AppConfig>);
}
@@ -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<Document> = {}) {
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<GitSyncConfig> = {}) {
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<GitSyncConfig>);
}
}
/**
* 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;

View File

@@ -22,6 +22,14 @@ export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
return $resultPromise;
}
/**
* RegisterModel 注册模型与表的映射关系
*/
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
return $resultPromise;
}
/**
* ServiceShutdown shuts down the service when the application closes
*/

View File

@@ -81,6 +81,14 @@ export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]
return $typingPromise;
}
/**
* LockDocument 锁定文档,防止删除
*/
export function LockDocument(id: number): Promise<void> & { 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<v
return $resultPromise;
}
/**
* UnlockDocument 解锁文档
*/
export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(222307930, id) as any;
return $resultPromise;
}
/**
* UpdateDocumentContent updates the content of a document
*/

View File

@@ -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 <K extends keyof GitSyncConfig>(key: K, value: GitSyncConfig[K]): Promise<void> => {
// 确保配置已加载
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<void> => {
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),
};
});

View File

@@ -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",
},
}
}

View File

@@ -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, // 默认不锁定
}
}

View File

@@ -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 扩展标识符

View File

@@ -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 快捷键命令

63
internal/models/sync.go Normal file
View File

@@ -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"`
}

View File

@@ -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()

View File

@@ -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)
}