feat: 初始化 kenaito-notebook 桌面记事本项目
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"kenaito-notebook-server/models"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewDatabase() (*Database, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
dbDir := filepath.Join(homeDir, ".kenaito-notebook")
|
||||
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dbDir, "notes.db")
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to enable WAL mode: %w", err)
|
||||
}
|
||||
|
||||
if err := createTables(db); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Database{db: db}, nil
|
||||
}
|
||||
|
||||
func createTables(db *sql.DB) error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
parent_id INTEGER DEFAULT 0,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
content TEXT DEFAULT '',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_folder BOOLEAN DEFAULT FALSE,
|
||||
tag TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_parent_id ON notes(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_sort_order ON notes(sort_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_deleted_at ON notes(deleted_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
hashed_password TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
|
||||
// 迁移:给已有表加 deleted_at 列
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name='deleted_at'").Scan(&count)
|
||||
if err == nil && count == 0 {
|
||||
_, err = db.Exec("ALTER TABLE notes ADD COLUMN deleted_at DATETIME DEFAULT NULL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移:给已有表加 user_id 列
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name='user_id'").Scan(&count)
|
||||
if err == nil && count == 0 {
|
||||
_, err = db.Exec("ALTER TABLE notes ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.Exec(query)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
// --- 笔记相关方法 ---
|
||||
|
||||
func (d *Database) CreateNote(userID int64, parentID int64, title string, isFolder bool) (*models.Note, error) {
|
||||
now := time.Now()
|
||||
result, err := d.db.Exec(`
|
||||
INSERT INTO notes (user_id, parent_id, title, is_folder, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, userID, parentID, title, isFolder, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.Note{
|
||||
ID: id,
|
||||
ParentID: parentID,
|
||||
Title: title,
|
||||
IsFolder: isFolder,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetNote(id int64) (*models.Note, error) {
|
||||
note := &models.Note{}
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||
FROM notes WHERE id = ?
|
||||
`, id).Scan(
|
||||
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return note, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetNotesByParentID(userID, parentID int64) ([]*models.Note, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||
FROM notes WHERE user_id = ? AND parent_id = ? AND deleted_at IS NULL
|
||||
ORDER BY sort_order, created_at
|
||||
`, userID, parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var notes []*models.Note
|
||||
for rows.Next() {
|
||||
note := &models.Note{}
|
||||
err := rows.Scan(
|
||||
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notes = append(notes, note)
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetAllNotes(userID int64) ([]*models.Note, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||
FROM notes WHERE user_id = ? AND deleted_at IS NULL AND is_folder = FALSE
|
||||
ORDER BY sort_order, created_at
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var notes []*models.Note
|
||||
for rows.Next() {
|
||||
note := &models.Note{}
|
||||
err := rows.Scan(
|
||||
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notes = append(notes, note)
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetAllNotesWithFolders(userID int64) ([]*models.Note, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||
FROM notes WHERE user_id = ? AND deleted_at IS NULL
|
||||
ORDER BY sort_order, created_at
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var notes []*models.Note
|
||||
for rows.Next() {
|
||||
note := &models.Note{}
|
||||
err := rows.Scan(
|
||||
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notes = append(notes, note)
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetFolderTree(userID, folderID int64) (*models.Note, error) {
|
||||
folder, err := d.GetNote(folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
children, err := d.GetNotesByParentID(userID, folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folder.Children = make([]*models.Note, len(children))
|
||||
for i, child := range children {
|
||||
if child.IsFolder {
|
||||
childFolder, err := d.GetFolderTree(userID, child.ID)
|
||||
if err == nil {
|
||||
folder.Children[i] = childFolder
|
||||
} else {
|
||||
folder.Children[i] = child
|
||||
}
|
||||
} else {
|
||||
folder.Children[i] = child
|
||||
}
|
||||
}
|
||||
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetAllNotesTree(userID int64) ([]*models.NoteTree, error) {
|
||||
notes, err := d.GetAllNotesWithFolders(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buildTree(notes, 0), nil
|
||||
}
|
||||
|
||||
func (d *Database) UpdateNote(id int64, title, content, tag string) error {
|
||||
now := time.Now()
|
||||
_, err := d.db.Exec(`
|
||||
UPDATE notes SET title = ?, content = ?, tag = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`, title, content, tag, now, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) DeleteNote(id int64) error {
|
||||
now := time.Now()
|
||||
|
||||
err := d.softDeleteChildren(id, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("UPDATE notes SET deleted_at = ? WHERE id = ?", now, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) softDeleteChildren(parentID int64, deletedAt time.Time) error {
|
||||
rows, err := d.db.Query("SELECT id FROM notes WHERE parent_id = ? AND deleted_at IS NULL", parentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.softDeleteChildren(id, deletedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("UPDATE notes SET deleted_at = ? WHERE parent_id = ? AND deleted_at IS NULL", deletedAt, parentID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) GetNoteTree(userID int64) ([]*models.NoteTree, error) {
|
||||
notes, err := d.getAllNotes(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buildTree(notes, 0), nil
|
||||
}
|
||||
|
||||
func (d *Database) getAllNotes(userID int64) ([]*models.Note, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||
FROM notes
|
||||
WHERE user_id = ? AND deleted_at IS NULL
|
||||
ORDER BY sort_order, created_at
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var notes []*models.Note
|
||||
for rows.Next() {
|
||||
note := &models.Note{}
|
||||
err := rows.Scan(
|
||||
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notes = append(notes, note)
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func buildTree(notes []*models.Note, parentID int64) []*models.NoteTree {
|
||||
var result []*models.NoteTree
|
||||
for _, note := range notes {
|
||||
if note.ParentID == parentID {
|
||||
node := &models.NoteTree{
|
||||
Note: *note,
|
||||
}
|
||||
node.Children = buildTree(notes, note.ID)
|
||||
result = append(result, node)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (d *Database) SearchNotes(userID int64, keyword string) ([]*models.Note, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||
FROM notes
|
||||
WHERE user_id = ? AND (title LIKE ? OR content LIKE ?) AND deleted_at IS NULL
|
||||
ORDER BY updated_at DESC
|
||||
`, userID, "%"+keyword+"%", "%"+keyword+"%")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var notes []*models.Note
|
||||
for rows.Next() {
|
||||
note := &models.Note{}
|
||||
err := rows.Scan(
|
||||
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notes = append(notes, note)
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func (d *Database) UpdateSortOrder(id int64, sortOrder int) error {
|
||||
_, err := d.db.Exec("UPDATE notes SET sort_order = ? WHERE id = ?", sortOrder, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) GetTrashNotes(userID int64) ([]*models.Note, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||
FROM notes
|
||||
WHERE user_id = ? AND deleted_at IS NOT NULL
|
||||
ORDER BY deleted_at DESC
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var notes []*models.Note
|
||||
for rows.Next() {
|
||||
note := &models.Note{}
|
||||
err := rows.Scan(
|
||||
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notes = append(notes, note)
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func (d *Database) RestoreNote(id int64) error {
|
||||
note, err := d.GetNote(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if note.IsFolder {
|
||||
err = d.restoreChildrenRecursive(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.db.Exec("UPDATE notes SET deleted_at = NULL WHERE id = ?", id)
|
||||
} else {
|
||||
_, err = d.db.Exec("UPDATE notes SET deleted_at = NULL, parent_id = 0 WHERE id = ?", id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) restoreChildrenRecursive(parentID int64) error {
|
||||
var parentDeletedAt *time.Time
|
||||
err := d.db.QueryRow("SELECT deleted_at FROM notes WHERE id = ?", parentID).Scan(&parentDeletedAt)
|
||||
if err != nil || parentDeletedAt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err := d.db.Query(
|
||||
"SELECT id, is_folder FROM notes WHERE parent_id = ? AND deleted_at = ?",
|
||||
parentID, *parentDeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type childInfo struct {
|
||||
id int64
|
||||
isFolder bool
|
||||
}
|
||||
var children []childInfo
|
||||
|
||||
for rows.Next() {
|
||||
var child childInfo
|
||||
if err := rows.Scan(&child.id, &child.isFolder); err != nil {
|
||||
return err
|
||||
}
|
||||
children = append(children, child)
|
||||
}
|
||||
|
||||
for _, child := range children {
|
||||
if child.isFolder {
|
||||
if err := d.restoreChildrenRecursive(child.id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(children) > 0 {
|
||||
_, err = d.db.Exec("UPDATE notes SET deleted_at = NULL WHERE parent_id = ? AND deleted_at = ?", parentID, *parentDeletedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) PermanentDeleteNote(id int64) error {
|
||||
err := d.permanentDeleteChildren(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("DELETE FROM notes WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) permanentDeleteChildren(parentID int64) error {
|
||||
rows, err := d.db.Query("SELECT id FROM notes WHERE parent_id = ?", parentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.permanentDeleteChildren(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("DELETE FROM notes WHERE parent_id = ?", parentID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) EmptyTrash(userID int64) error {
|
||||
_, err := d.db.Exec("DELETE FROM notes WHERE user_id = ? AND deleted_at IS NOT NULL", userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// --- 用户相关方法 ---
|
||||
|
||||
type UserRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
HashedPassword string `json:"-"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (d *Database) CreateUser(email, hashedPassword string) (*UserRecord, error) {
|
||||
now := time.Now()
|
||||
result, err := d.db.Exec(`
|
||||
INSERT INTO users (email, hashed_password, created_at)
|
||||
VALUES (?, ?, ?)
|
||||
`, email, hashedPassword, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UserRecord{
|
||||
ID: id,
|
||||
HashedPassword: hashedPassword,
|
||||
Email: email,
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetUserByEmail(email string) (*UserRecord, error) {
|
||||
user := &UserRecord{}
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, hashed_password, email, created_at
|
||||
FROM users WHERE email = ?
|
||||
`, email).Scan(&user.ID, &user.HashedPassword, &user.Email, &user.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetUserByID(id int64) (*UserRecord, error) {
|
||||
user := &UserRecord{}
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, hashed_password, email, created_at
|
||||
FROM users WHERE id = ?
|
||||
`, id).Scan(&user.ID, &user.HashedPassword, &user.Email, &user.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (d *Database) UpdatePassword(id int64, hashedPassword string) error {
|
||||
_, err := d.db.Exec("UPDATE users SET hashed_password = ? WHERE id = ?", hashedPassword, id)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user