b93c6f1e16
- FTS5中文分词搜索(需-tags=sqlite_fts5) - [[wiki链接]]反向链接+SVG知识图谱 - 自动保存草稿(不触发版本历史) - 标签重命名/合并/删除+使用统计 - 后台编辑实时预览+格式工具栏 - PWA manifest+service worker+移动端适配 - 冒烟测试扩至66例全通过
654 lines
21 KiB
Go
654 lines
21 KiB
Go
package repository
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"gorm.io/driver/sqlite"
|
||
"gorm.io/gorm"
|
||
"note-manager/model"
|
||
)
|
||
|
||
// NoteRepository 笔记数据访问层
|
||
type NoteRepository struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewNoteRepository 创建数据仓库实例,初始化数据库
|
||
func NewNoteRepository(dbPath string) (*NoteRepository, error) {
|
||
// 确保数据库目录存在
|
||
dir := filepath.Dir(dbPath)
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||
}
|
||
|
||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||
}
|
||
|
||
// 开启外键约束(SQLite 默认关闭)
|
||
db.Exec("PRAGMA foreign_keys = ON")
|
||
|
||
// 自动迁移表结构
|
||
if err := db.AutoMigrate(&model.Note{}, &model.NoteVersion{}); err != nil {
|
||
return nil, fmt.Errorf("数据库迁移失败: %w", err)
|
||
}
|
||
|
||
repo := &NoteRepository{db: db}
|
||
|
||
// 初始化 FTS5 全文搜索(含中文 trigram 分词)
|
||
if err := repo.initFTS5(); err != nil {
|
||
return nil, fmt.Errorf("初始化全文搜索失败: %w", err)
|
||
}
|
||
|
||
return repo, nil
|
||
}
|
||
|
||
// ─────────────── FTS5 全文搜索 ───────────────
|
||
|
||
// initFTS5 创建 FTS5 虚拟表并全量重建索引。
|
||
// 采用 unicode61 分词器 + 在 Go 层对中文做逐字分词(segmentCJK),
|
||
// 使中文搜索支持 1~2 个字符(trigram 只支持 3 字以上,不适合中文短词)。
|
||
func (r *NoteRepository) initFTS5() error {
|
||
// 独立 FTS5 表(不依赖外部内容表,由 Go 代码维护,便于中文分词)
|
||
createSQL := `
|
||
CREATE VIRTUAL TABLE IF NOT EXISTS note_search USING fts5(
|
||
title, content, category, tags,
|
||
tokenize='unicode61 remove_diacritics 2'
|
||
);`
|
||
if err := r.db.Exec(createSQL).Error; err != nil {
|
||
return err
|
||
}
|
||
// 启动时全量重建索引(低成本,保证索引与数据一致)
|
||
return r.rebuildAllFTS()
|
||
}
|
||
|
||
// segmentCJK 在中文/日文/韩文等连写字符之间插入空格,并在 CJK 与 ASCII 之间也插入空格,
|
||
// 使 unicode61 分词器能把每个汉字作为独立词元索引(支持短词),同时保留英文单词。
|
||
func segmentCJK(s string) string {
|
||
if s == "" {
|
||
return ""
|
||
}
|
||
isCJKChar := func(r rune) bool {
|
||
return (r >= 0x4E00 && r <= 0x9FFF) || // CJK 统一表意文字
|
||
(r >= 0x3040 && r <= 0x30FF) || // 日文平假名/片假名
|
||
(r >= 0xAC00 && r <= 0xD7AF) || // 韩文
|
||
(r >= 0x3400 && r <= 0x4DBF) // CJK 扩展 A
|
||
}
|
||
var b strings.Builder
|
||
first := true
|
||
var prevIsCJK bool
|
||
for _, r := range s {
|
||
cur := isCJKChar(r)
|
||
if !first && cur != prevIsCJK {
|
||
b.WriteRune(' ')
|
||
} else if !first && cur && prevIsCJK {
|
||
// 连续 CJK 字符之间也加空格,使每个字独立
|
||
b.WriteRune(' ')
|
||
}
|
||
b.WriteRune(r)
|
||
prevIsCJK = cur
|
||
first = false
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// rebuildNoteFTS 重建某篇笔记的全文索引行(先删后插)
|
||
func (r *NoteRepository) rebuildNoteFTS(id uint) error {
|
||
var note model.Note
|
||
if err := r.db.Unscoped().First(¬e, id).Error; err != nil {
|
||
// 笔记不存在则清除索引行
|
||
_ = r.db.Exec(`DELETE FROM note_search WHERE rowid = ?`, id).Error
|
||
return nil
|
||
}
|
||
sTitle := segmentCJK(note.Title)
|
||
sContent := segmentCJK(note.Content)
|
||
sCategory := segmentCJK(note.Category)
|
||
sTags := segmentCJK(note.Tags)
|
||
if err := r.db.Exec(`DELETE FROM note_search WHERE rowid = ?`, id).Error; err != nil {
|
||
return err
|
||
}
|
||
return r.db.Exec(`
|
||
INSERT INTO note_search(rowid, title, content, category, tags)
|
||
VALUES (?, ?, ?, ?, ?)`,
|
||
id, sTitle, sContent, sCategory, sTags).Error
|
||
}
|
||
|
||
// rebuildAllFTS 全量重建所有未删除笔记的索引(先清空再重建)
|
||
func (r *NoteRepository) rebuildAllFTS() error {
|
||
// 常规 FTS5 表用 DELETE 清空所有行
|
||
if err := r.db.Exec(`DELETE FROM note_search`).Error; err != nil {
|
||
return err
|
||
}
|
||
var ids []uint
|
||
if err := r.db.Model(&model.Note{}).Where("is_folder = ?", false).Pluck("id", &ids).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, id := range ids {
|
||
if err := r.rebuildNoteFTS(id); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// FTS5Search 使用全文索引搜索(同时支持英文与中文短词)
|
||
// 返回匹配的笔记列表(排除已删除与目录)
|
||
func (r *NoteRepository) FTS5Search(keyword string, page, pageSize int) ([]model.NoteListItem, int64, error) {
|
||
// 查询词同样做中文逐字分词,与索引侧保持一致
|
||
keyword = strings.TrimSpace(segmentCJK(keyword))
|
||
if keyword == "" {
|
||
return nil, 0, nil
|
||
}
|
||
|
||
var total int64
|
||
// 先统计匹配总数(通过 FTS 表 join 到 notes 过滤目录与已删除)
|
||
countQuery := `
|
||
SELECT COUNT(*) FROM note_search s
|
||
JOIN notes n ON n.id = s.rowid
|
||
WHERE note_search MATCH ? AND n.is_folder = 0
|
||
AND (n.deleted_at IS NULL OR n.deleted_at = '')`
|
||
if err := r.db.Raw(countQuery, keyword).Scan(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
offset := (page - 1) * pageSize
|
||
var items []model.NoteListItem
|
||
err := r.db.Raw(`
|
||
SELECT n.id, n.title, n.category, n.tags,
|
||
CASE WHEN n.password != '' THEN 1 ELSE 0 END AS has_password,
|
||
n.is_pinned, n.is_favorite, n.is_public, n.parent_id, n.is_folder,
|
||
n.sort_order, n.visit_count, n.created_at, n.updated_at
|
||
FROM note_search s
|
||
JOIN notes n ON n.id = s.rowid
|
||
WHERE note_search MATCH ? AND n.is_folder = 0
|
||
AND (n.deleted_at IS NULL OR n.deleted_at = '')
|
||
ORDER BY bm25(note_search), n.updated_at DESC
|
||
LIMIT ? OFFSET ?`,
|
||
keyword, pageSize, offset,
|
||
).Scan(&items).Error
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return items, total, nil
|
||
}
|
||
|
||
// Create 创建笔记
|
||
func (r *NoteRepository) Create(note *model.Note) error {
|
||
res := r.db.Select("Title", "Content", "DraftContent", "Category", "Tags", "Password", "IsPinned", "IsFavorite", "IsPublic", "ParentID", "IsFolder", "SortOrder", "ShareToken", "ShareExpireAt", "VisitCount").Create(note)
|
||
if res.Error != nil {
|
||
return res.Error
|
||
}
|
||
// 建立全文索引(目录不索引)
|
||
if !note.IsFolder {
|
||
return r.rebuildNoteFTS(note.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetByID 根据 ID 获取笔记(排除已删除)
|
||
func (r *NoteRepository) GetByID(id uint) (*model.Note, error) {
|
||
var note model.Note
|
||
err := r.db.First(¬e, id).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ¬e, nil
|
||
}
|
||
|
||
// GetByIDIncludingDeleted 获取笔记(包含已软删除的,用于回收站恢复)
|
||
func (r *NoteRepository) GetByIDIncludingDeleted(id uint) (*model.Note, error) {
|
||
var note model.Note
|
||
err := r.db.Unscoped().First(¬e, id).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ¬e, nil
|
||
}
|
||
|
||
// Update 更新笔记
|
||
func (r *NoteRepository) Update(note *model.Note) error {
|
||
if err := r.db.Save(note).Error; err != nil {
|
||
return err
|
||
}
|
||
// 更新全文索引(目录不索引)
|
||
if !note.IsFolder {
|
||
return r.rebuildNoteFTS(note.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// UpdateFields 按字段更新(避免 Save 覆盖所有字段)
|
||
func (r *NoteRepository) UpdateFields(id uint, fields map[string]interface{}) error {
|
||
if err := r.db.Model(&model.Note{}).Where("id = ?", id).Updates(fields).Error; err != nil {
|
||
return err
|
||
}
|
||
// 若更新涉及可索引字段(标题/内容/分类/标签),同步重建索引
|
||
if _, ok := fields["Title"]; ok {
|
||
return r.rebuildNoteFTS(id)
|
||
}
|
||
if _, ok := fields["Content"]; ok {
|
||
return r.rebuildNoteFTS(id)
|
||
}
|
||
if _, ok := fields["Category"]; ok {
|
||
return r.rebuildNoteFTS(id)
|
||
}
|
||
if _, ok := fields["Tags"]; ok {
|
||
return r.rebuildNoteFTS(id)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Delete 软删除笔记
|
||
func (r *NoteRepository) Delete(id uint) error {
|
||
return r.db.Delete(&model.Note{}, id).Error
|
||
}
|
||
|
||
// ListQuery 列表查询参数
|
||
type ListQuery struct {
|
||
Page int
|
||
PageSize int
|
||
Category string
|
||
Tag string
|
||
Pinned *bool
|
||
Favorite *bool
|
||
ParentID *uint // 父目录 ID,nil 表示所有
|
||
}
|
||
|
||
// List 获取笔记列表(分页)
|
||
func (r *NoteRepository) List(q ListQuery) ([]model.NoteListItem, int64, error) {
|
||
var items []model.NoteListItem
|
||
var total int64
|
||
|
||
query := r.db.Model(&model.Note{})
|
||
|
||
if q.Category != "" {
|
||
query = query.Where("category = ?", q.Category)
|
||
}
|
||
if q.Tag != "" {
|
||
query = query.Where("tags LIKE ?", fmt.Sprintf("%%\"%s\"%%", q.Tag))
|
||
}
|
||
if q.Pinned != nil {
|
||
query = query.Where("is_pinned = ?", *q.Pinned)
|
||
}
|
||
if q.Favorite != nil {
|
||
query = query.Where("is_favorite = ?", *q.Favorite)
|
||
}
|
||
if q.ParentID != nil {
|
||
query = query.Where("parent_id = ?", *q.ParentID)
|
||
}
|
||
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
offset := (q.Page - 1) * q.PageSize
|
||
err := query.Select("id, title, category, tags, is_pinned, is_favorite, is_public, parent_id, is_folder, sort_order, visit_count, created_at, updated_at").
|
||
Order("is_folder DESC, sort_order ASC, updated_at DESC").
|
||
Offset(offset).
|
||
Limit(q.PageSize).
|
||
Find(&items).Error
|
||
|
||
return items, total, err
|
||
}
|
||
|
||
// GetAllTree 获取所有笔记的树形结构(含分享信息)
|
||
func (r *NoteRepository) GetAllTree() ([]model.NoteListItem, error) {
|
||
var items []model.NoteListItem
|
||
err := r.db.Model(&model.Note{}).
|
||
Select("id, title, category, tags, CASE WHEN password != '' THEN 1 ELSE 0 END as has_password, is_pinned, is_favorite, is_public, parent_id, is_folder, sort_order, share_token, share_expire_at, visit_count, created_at, updated_at").
|
||
Order("is_folder DESC, sort_order ASC, title ASC").
|
||
Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
// GetPublicTree 获取公开可见的树形结构(前台用,不含分享令牌)
|
||
func (r *NoteRepository) GetPublicTree() ([]model.NoteListItem, error) {
|
||
var items []model.NoteListItem
|
||
err := r.db.Model(&model.Note{}).
|
||
Select("id, title, category, tags, CASE WHEN password != '' THEN 1 ELSE 0 END as has_password, is_pinned, is_favorite, is_public, parent_id, is_folder, sort_order, created_at, updated_at").
|
||
Order("is_folder DESC, sort_order ASC, title ASC").
|
||
Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
// GetByParentID 获取指定父目录下的所有项目(排除已删除)
|
||
func (r *NoteRepository) GetByParentID(parentID uint) ([]model.NoteListItem, error) {
|
||
var items []model.NoteListItem
|
||
err := r.db.Model(&model.Note{}).
|
||
Where("parent_id = ?", parentID).
|
||
Select("id, title, category, tags, is_pinned, is_favorite, is_public, parent_id, is_folder, sort_order, created_at, updated_at").
|
||
Order("is_folder DESC, sort_order ASC, title ASC").
|
||
Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
// GetChildrenCount 获取子项数量(排除已删除)
|
||
func (r *NoteRepository) GetChildrenCount(parentID uint) (int64, error) {
|
||
var count int64
|
||
err := r.db.Model(&model.Note{}).Where("parent_id = ?", parentID).Count(&count).Error
|
||
return count, err
|
||
}
|
||
|
||
// DeleteWithChildren 软删除目录及其下所有内容
|
||
func (r *NoteRepository) DeleteWithChildren(id uint) error {
|
||
// 递归收集所有后代 id
|
||
ids := r.collectDescendants(id)
|
||
var err error
|
||
if len(ids) > 0 {
|
||
err = r.db.Delete(&model.Note{}, ids).Error
|
||
} else {
|
||
err = r.db.Delete(&model.Note{}, id).Error
|
||
}
|
||
return err
|
||
}
|
||
|
||
// collectDescendants 收集目录的所有后代 ID(含自身)
|
||
func (r *NoteRepository) collectDescendants(id uint) []uint {
|
||
ids := []uint{id}
|
||
queue := []uint{id}
|
||
for len(queue) > 0 {
|
||
parent := queue[0]
|
||
queue = queue[1:]
|
||
var children []uint
|
||
if err := r.db.Model(&model.Note{}).Where("parent_id = ? AND is_folder = ?", parent, true).Pluck("id", &children).Error; err != nil {
|
||
continue
|
||
}
|
||
for _, c := range children {
|
||
ids = append(ids, c)
|
||
queue = append(queue, c)
|
||
}
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// ListTrash 回收站列表(只含已软删除项)
|
||
func (r *NoteRepository) ListTrash() ([]model.NoteListItem, error) {
|
||
var items []model.NoteListItem
|
||
err := r.db.Unscoped().Model(&model.Note{}).
|
||
Where("deleted_at IS NOT NULL").
|
||
Select("id, title, category, tags, is_pinned, is_favorite, is_public, parent_id, is_folder, sort_order, visit_count, created_at, updated_at").
|
||
Order("deleted_at DESC").
|
||
Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
// Restore 从回收站恢复(连带恢复已被软删除的父目录路径不需要特殊处理)
|
||
func (r *NoteRepository) Restore(id uint) error {
|
||
// 恢复自身
|
||
if err := r.db.Unscoped().Model(&model.Note{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return err
|
||
}
|
||
// 若其祖先目录也被软删除,一并恢复
|
||
var note model.Note
|
||
if err := r.db.Unscoped().First(¬e, id).Error; err == nil && note.ParentID != 0 {
|
||
var parent model.Note
|
||
if err := r.db.Unscoped().First(&parent, note.ParentID).Error; err == nil && !parent.DeletedAt.Time.IsZero() {
|
||
_ = r.db.Unscoped().Model(&model.Note{}).Where("id = ?", parent.ID).Update("deleted_at", nil).Error
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// RestoreSubtree 恢复整个子树(含所有已软删除后代),返回恢复的节点数
|
||
func (r *NoteRepository) RestoreSubtree(rootID uint) error {
|
||
// 收集整棵子树所有节点 id(Unscoped,含已删除)
|
||
var ids []uint
|
||
queue := []uint{rootID}
|
||
for len(queue) > 0 {
|
||
parent := queue[0]
|
||
queue = queue[1:]
|
||
var children []uint
|
||
if err := r.db.Unscoped().Model(&model.Note{}).Where("parent_id = ?", parent).Pluck("id", &children).Error; err != nil {
|
||
return err
|
||
}
|
||
ids = append(ids, children...)
|
||
queue = append(queue, children...)
|
||
}
|
||
// 统一恢复所有节点(含自身)
|
||
ids = append(ids, rootID)
|
||
return r.db.Unscoped().Model(&model.Note{}).Where("id IN ?", ids).Update("deleted_at", nil).Error
|
||
}
|
||
|
||
// HardDelete 彻底删除(不可恢复)
|
||
func (r *NoteRepository) HardDelete(id uint) error {
|
||
return r.db.Unscoped().Delete(&model.Note{}, id).Error
|
||
}
|
||
|
||
// HardDeleteWithChildren 彻底删除目录及所有后代
|
||
func (r *NoteRepository) HardDeleteWithChildren(id uint) error {
|
||
ids := r.collectDescendantsIncludingDeleted(id)
|
||
ids = append(ids, id)
|
||
return r.db.Unscoped().Delete(&model.Note{}, ids).Error
|
||
}
|
||
|
||
func (r *NoteRepository) collectDescendantsIncludingDeleted(id uint) []uint {
|
||
var ids []uint
|
||
queue := []uint{id}
|
||
for len(queue) > 0 {
|
||
parent := queue[0]
|
||
queue = queue[1:]
|
||
var children []uint
|
||
if err := r.db.Unscoped().Model(&model.Note{}).Where("parent_id = ? AND is_folder = ?", parent, true).Pluck("id", &children).Error; err != nil {
|
||
continue
|
||
}
|
||
for _, c := range children {
|
||
ids = append(ids, c)
|
||
queue = append(queue, c)
|
||
}
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// Search 搜索笔记(按标题和内容)
|
||
func (r *NoteRepository) Search(keyword string, page, pageSize int) ([]model.NoteListItem, int64, error) {
|
||
var items []model.NoteListItem
|
||
var total int64
|
||
|
||
like := "%" + keyword + "%"
|
||
query := r.db.Model(&model.Note{}).Where("(title LIKE ? OR content LIKE ?) AND is_folder = ?", like, like, false)
|
||
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := query.Select("id, title, category, tags, is_pinned, is_favorite, is_public, parent_id, is_folder, sort_order, visit_count, created_at, updated_at").
|
||
Order("is_pinned DESC, updated_at DESC").
|
||
Offset(offset).
|
||
Limit(pageSize).
|
||
Find(&items).Error
|
||
|
||
return items, total, err
|
||
}
|
||
|
||
// GetCategories 获取所有分类(排除已删除和目录)
|
||
func (r *NoteRepository) GetCategories() ([]string, error) {
|
||
var categories []string
|
||
err := r.db.Model(&model.Note{}).
|
||
Distinct("category").
|
||
Where("category != '' AND is_folder = ?", false).
|
||
Pluck("category", &categories).Error
|
||
return categories, err
|
||
}
|
||
|
||
// GetTags 获取所有标签(排除已删除和目录)
|
||
func (r *NoteRepository) GetTags() ([]string, error) {
|
||
var tagsJSON []string
|
||
err := r.db.Model(&model.Note{}).
|
||
Where("tags != '' AND tags IS NOT NULL AND is_folder = ?", false).
|
||
Pluck("tags", &tagsJSON).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 去重
|
||
seen := make(map[string]bool)
|
||
var result []string
|
||
for _, t := range tagsJSON {
|
||
if !seen[t] {
|
||
seen[t] = true
|
||
result = append(result, t)
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// ─────────────── 标签管理 ───────────────
|
||
|
||
// UpdateTagAll 将所有笔记中出现的指定标签重命名/合并/删除。
|
||
// oldTag 为要操作的旧标签;newTag 传入新名称实现重命名,传空字符串则删除该标签。
|
||
func (r *NoteRepository) UpdateTagAll(oldTag, newTag string) (int64, error) {
|
||
var notes []model.Note
|
||
if err := r.db.Where("tags LIKE ?", fmt.Sprintf("%%\"%s\"%%", oldTag)).
|
||
Where("is_folder = ?", false).
|
||
Find(¬es).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
changed := int64(0)
|
||
for i := range notes {
|
||
var tagSlice []string
|
||
if json.Unmarshal([]byte(notes[i].Tags), &tagSlice) != nil {
|
||
continue
|
||
}
|
||
// 过滤掉旧标签;若 newTag 非空则加入新标签(避免重复)
|
||
seen := make(map[string]bool)
|
||
var newSlice []string
|
||
for _, t := range tagSlice {
|
||
if t == oldTag {
|
||
changed++
|
||
if newTag != "" && !seen[newTag] {
|
||
newSlice = append(newSlice, newTag)
|
||
seen[newTag] = true
|
||
}
|
||
continue
|
||
}
|
||
if !seen[t] {
|
||
newSlice = append(newSlice, t)
|
||
seen[t] = true
|
||
}
|
||
}
|
||
newJSON, _ := json.Marshal(newSlice)
|
||
if err := r.db.Model(&model.Note{}).Where("id = ?", notes[i].ID).
|
||
Update("tags", string(newJSON)).Error; err != nil {
|
||
return changed, err
|
||
}
|
||
// 标签变化,同步全文索引
|
||
_ = r.rebuildNoteFTS(notes[i].ID)
|
||
}
|
||
return changed, nil
|
||
}
|
||
|
||
// ─────────────── 双向链接 / 知识图谱 ───────────────
|
||
|
||
// GetAllNotesLight 获取所有未删除笔记的 id、标题 与 标签(用于解析 [[wiki链接]] 与标签统计)
|
||
func (r *NoteRepository) GetAllNotesLight() ([]model.NoteListItem, error) {
|
||
var items []model.NoteListItem
|
||
err := r.db.Model(&model.Note{}).
|
||
Where("is_folder = ?", false).
|
||
Select("id, title, tags").Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
// GetAllLinks 获取所有未删除笔记的 id、标题、内容(用于扫描双向链接与构建图谱)
|
||
// 仅返回轻量字段以降低内存占用
|
||
func (r *NoteRepository) GetAllContentLight() ([]struct {
|
||
ID uint `gorm:"column:id"`
|
||
Title string `gorm:"column:title"`
|
||
Content string `gorm:"column:content"`
|
||
}, error) {
|
||
var items []struct {
|
||
ID uint `gorm:"column:id"`
|
||
Title string `gorm:"column:title"`
|
||
Content string `gorm:"column:content"`
|
||
}
|
||
err := r.db.Model(&model.Note{}).
|
||
Where("is_folder = ?", false).
|
||
Select("id, title, content").Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
// GetByIDs 批量获取笔记(用于解析链接指向的笔记是否存在)
|
||
func (r *NoteRepository) GetByIDs(ids []uint) ([]model.NoteListItem, error) {
|
||
if len(ids) == 0 {
|
||
return nil, nil
|
||
}
|
||
var items []model.NoteListItem
|
||
err := r.db.Model(&model.Note{}).
|
||
Where("id IN ?", ids).
|
||
Select("id, title").Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
// ─────────────── 版本历史 ───────────────
|
||
|
||
// SaveVersion 保存笔记新版本快照
|
||
func (r *NoteRepository) SaveVersion(note *model.Note) (*model.NoteVersion, error) {
|
||
v := &model.NoteVersion{
|
||
NoteID: note.ID,
|
||
Title: note.Title,
|
||
Content: note.Content,
|
||
Category: note.Category,
|
||
Tags: note.Tags,
|
||
}
|
||
if err := r.db.Create(v).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
return v, nil
|
||
}
|
||
|
||
// ListVersions 获取笔记的所有版本(按时间倒序)
|
||
func (r *NoteRepository) ListVersions(noteID uint) ([]model.NoteVersion, error) {
|
||
var versions []model.NoteVersion
|
||
err := r.db.Where("note_id = ?", noteID).Order("created_at DESC").Find(&versions).Error
|
||
return versions, err
|
||
}
|
||
|
||
// GetVersion 获取指定版本
|
||
func (r *NoteRepository) GetVersion(id uint) (*model.NoteVersion, error) {
|
||
var v model.NoteVersion
|
||
err := r.db.First(&v, id).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &v, nil
|
||
}
|
||
|
||
// DeleteVersions 删除某笔记的全部版本
|
||
func (r *NoteRepository) DeleteVersions(noteID uint) error {
|
||
return r.db.Where("note_id = ?", noteID).Delete(&model.NoteVersion{}).Error
|
||
}
|
||
|
||
// ─────────────── 分享 ───────────────
|
||
|
||
// GetByShareToken 通过分享令牌获取笔记(含已删除?分享的笔记不应是已删除的,排除)
|
||
func (r *NoteRepository) GetByShareToken(token string) (*model.Note, error) {
|
||
var note model.Note
|
||
err := r.db.Where("share_token = ?", token).First(¬e).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New("分享链接无效或已失效")
|
||
}
|
||
return nil, err
|
||
}
|
||
return ¬e, nil
|
||
}
|
||
|
||
// IncrementVisit 增加浏览次数
|
||
func (r *NoteRepository) IncrementVisit(id uint) error {
|
||
return r.db.Model(&model.Note{}).Where("id = ?", id).
|
||
UpdateColumn("visit_count", gorm.Expr("visit_count + 1")).Error
|
||
}
|
||
|
||
// IncrementVisitByToken 通过分享令牌增加浏览次数
|
||
func (r *NoteRepository) IncrementVisitByToken(token string) error {
|
||
return r.db.Model(&model.Note{}).Where("share_token = ?", token).
|
||
UpdateColumn("visit_count", gorm.Expr("visit_count + 1")).Error
|
||
}
|