|
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
@@ -25,12 +26,14 @@ CREATE TABLE IF NOT EXISTS documents (
|
|
|
|
|
title TEXT NOT NULL,
|
|
|
|
|
content TEXT DEFAULT '∞∞∞text-a',
|
|
|
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
is_deleted INTEGER DEFAULT 0
|
|
|
|
|
)`
|
|
|
|
|
|
|
|
|
|
// 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)`
|
|
|
|
|
sqlCreateIndexIsDeleted = `CREATE INDEX IF NOT EXISTS idx_documents_is_deleted ON documents(is_deleted)`
|
|
|
|
|
|
|
|
|
|
// SQLite performance optimization settings
|
|
|
|
|
sqlOptimizationSettings = `
|
|
|
|
@@ -42,18 +45,13 @@ PRAGMA foreign_keys = ON;`
|
|
|
|
|
|
|
|
|
|
// Document operations
|
|
|
|
|
sqlGetDocumentByID = `
|
|
|
|
|
SELECT id, title, content, created_at, updated_at
|
|
|
|
|
SELECT id, title, content, created_at, updated_at, is_deleted
|
|
|
|
|
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 = ?`
|
|
|
|
|
INSERT INTO documents (title, content, created_at, updated_at, is_deleted)
|
|
|
|
|
VALUES (?, ?, ?, ?, 0)`
|
|
|
|
|
|
|
|
|
|
sqlUpdateDocumentContent = `
|
|
|
|
|
UPDATE documents
|
|
|
|
@@ -65,16 +63,34 @@ UPDATE documents
|
|
|
|
|
SET title = ?, updated_at = ?
|
|
|
|
|
WHERE id = ?`
|
|
|
|
|
|
|
|
|
|
sqlDeleteDocument = `
|
|
|
|
|
DELETE FROM documents WHERE id = ?`
|
|
|
|
|
sqlMarkDocumentAsDeleted = `
|
|
|
|
|
UPDATE documents
|
|
|
|
|
SET is_deleted = 1, updated_at = ?
|
|
|
|
|
WHERE id = ?`
|
|
|
|
|
|
|
|
|
|
sqlRestoreDocument = `
|
|
|
|
|
UPDATE documents
|
|
|
|
|
SET is_deleted = 0, updated_at = ?
|
|
|
|
|
WHERE id = ?`
|
|
|
|
|
|
|
|
|
|
sqlListAllDocumentsMeta = `
|
|
|
|
|
SELECT id, title, created_at, updated_at
|
|
|
|
|
FROM documents
|
|
|
|
|
WHERE is_deleted = 0
|
|
|
|
|
ORDER BY updated_at DESC`
|
|
|
|
|
|
|
|
|
|
sqlListDeletedDocumentsMeta = `
|
|
|
|
|
SELECT id, title, created_at, updated_at
|
|
|
|
|
FROM documents
|
|
|
|
|
WHERE is_deleted = 1
|
|
|
|
|
ORDER BY updated_at DESC`
|
|
|
|
|
|
|
|
|
|
sqlGetFirstDocumentID = `
|
|
|
|
|
SELECT id FROM documents ORDER BY id LIMIT 1`
|
|
|
|
|
SELECT id FROM documents WHERE is_deleted = 0 ORDER BY id LIMIT 1`
|
|
|
|
|
|
|
|
|
|
sqlCountDocuments = `SELECT COUNT(*) FROM documents WHERE is_deleted = 0`
|
|
|
|
|
|
|
|
|
|
sqlDefaultDocumentID = 1 // 默认文档的ID
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// DocumentService provides document management functionality
|
|
|
|
@@ -111,6 +127,23 @@ func (ds *DocumentService) initDatabase() error {
|
|
|
|
|
return fmt.Errorf("failed to get database path: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保数据库目录存在
|
|
|
|
|
dbDir := filepath.Dir(dbPath)
|
|
|
|
|
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create database directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查数据库文件是否存在,如果不存在则创建
|
|
|
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
|
|
|
ds.logger.Info("Database file does not exist, creating empty file", "path", dbPath)
|
|
|
|
|
// 创建空文件
|
|
|
|
|
file, err := os.Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create database file: %w", err)
|
|
|
|
|
}
|
|
|
|
|
file.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to open database: %w", err)
|
|
|
|
@@ -155,6 +188,7 @@ func (ds *DocumentService) createIndexes() error {
|
|
|
|
|
indexes := []string{
|
|
|
|
|
sqlCreateIndexUpdatedAt,
|
|
|
|
|
sqlCreateIndexTitle,
|
|
|
|
|
sqlCreateIndexIsDeleted,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, index := range indexes {
|
|
|
|
@@ -169,7 +203,7 @@ func (ds *DocumentService) createIndexes() error {
|
|
|
|
|
func (ds *DocumentService) ensureDefaultDocument() error {
|
|
|
|
|
// Check if any document exists
|
|
|
|
|
var count int
|
|
|
|
|
err := ds.db.QueryRow("SELECT COUNT(*) FROM documents").Scan(&count)
|
|
|
|
|
err := ds.db.QueryRow(sqlCountDocuments).Scan(&count)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -189,14 +223,16 @@ func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) {
|
|
|
|
|
defer ds.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
var doc models.Document
|
|
|
|
|
var isDeletedInt int
|
|
|
|
|
row := ds.db.QueryRow(sqlGetDocumentByID, id)
|
|
|
|
|
err := row.Scan(&doc.ID, &doc.Title, &doc.Content, &doc.CreatedAt, &doc.UpdatedAt)
|
|
|
|
|
err := row.Scan(&doc.ID, &doc.Title, &doc.Content, &doc.CreatedAt, &doc.UpdatedAt, &isDeletedInt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("failed to get document by ID: %w", err)
|
|
|
|
|
}
|
|
|
|
|
doc.IsDeleted = isDeletedInt == 1
|
|
|
|
|
|
|
|
|
|
return &doc, nil
|
|
|
|
|
}
|
|
|
|
@@ -213,6 +249,7 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
|
|
|
|
Content: "∞∞∞text-a\n",
|
|
|
|
|
CreatedAt: now,
|
|
|
|
|
UpdatedAt: now,
|
|
|
|
|
IsDeleted: false,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result, err := ds.db.Exec(sqlInsertDocument, doc.Title, doc.Content, doc.CreatedAt, doc.UpdatedAt)
|
|
|
|
@@ -255,31 +292,36 @@ func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeleteDocument deletes a document (not allowed if it's the only document)
|
|
|
|
|
// DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
|
|
|
|
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)
|
|
|
|
|
// 不允许删除默认文档
|
|
|
|
|
if id == sqlDefaultDocumentID {
|
|
|
|
|
return fmt.Errorf("cannot delete the default document")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
_, err := ds.db.Exec(sqlMarkDocumentAsDeleted, time.Now(), id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to delete document: %w", err)
|
|
|
|
|
return fmt.Errorf("failed to mark document as deleted: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListAllDocumentsMeta lists all document metadata
|
|
|
|
|
// RestoreDocument restores a deleted document
|
|
|
|
|
func (ds *DocumentService) RestoreDocument(id int64) error {
|
|
|
|
|
ds.mu.Lock()
|
|
|
|
|
defer ds.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
_, err := ds.db.Exec(sqlRestoreDocument, time.Now(), id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to restore document: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListAllDocumentsMeta lists all active (non-deleted) document metadata
|
|
|
|
|
func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) {
|
|
|
|
|
ds.mu.RLock()
|
|
|
|
|
defer ds.mu.RUnlock()
|
|
|
|
@@ -297,13 +339,39 @@ func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to scan document meta: %w", err)
|
|
|
|
|
}
|
|
|
|
|
doc.IsDeleted = false
|
|
|
|
|
documents = append(documents, &doc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return documents, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetFirstDocumentID gets the first document's ID for frontend initialization
|
|
|
|
|
// ListDeletedDocumentsMeta lists all deleted document metadata
|
|
|
|
|
func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error) {
|
|
|
|
|
ds.mu.RLock()
|
|
|
|
|
defer ds.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
rows, err := ds.db.Query(sqlListDeletedDocumentsMeta)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to list deleted 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 deleted document meta: %w", err)
|
|
|
|
|
}
|
|
|
|
|
doc.IsDeleted = true
|
|
|
|
|
documents = append(documents, &doc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return documents, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetFirstDocumentID gets the first active document's ID for frontend initialization
|
|
|
|
|
func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
|
|
|
|
ds.mu.RLock()
|
|
|
|
|
defer ds.mu.RUnlock()
|
|
|
|
|