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{}, &model.User{}); 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 使用全文索引搜索(同时支持英文与中文短词) // 返回匹配的笔记列表(排除已删除与目录),并按 userID 做数据隔离 func (r *NoteRepository) FTS5Search(userID uint, 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.user_id = ? AND (n.deleted_at IS NULL OR n.deleted_at = '')` if err := r.db.Raw(countQuery, keyword, userID).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.user_id = ? AND (n.deleted_at IS NULL OR n.deleted_at = '') ORDER BY bm25(note_search), n.updated_at DESC LIMIT ? OFFSET ?`, keyword, userID, 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("UserID", "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 } // GetByIDScoped 根据 ID 获取笔记并校验归属(多租户隔离,防止跨用户访问) func (r *NoteRepository) GetByIDScoped(userID, id uint) (*model.Note, error) { var note model.Note err := r.db.Where("user_id = ?", userID).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 } // Scoped 返回带用户隔离的查询(供需要复用 db 的场景) func (r *NoteRepository) Scoped(userID uint) *gorm.DB { return r.db.Where("user_id = ?", userID) } // DB 暴露底层数据库连接(供其它仓库复用) func (r *NoteRepository) DB() *gorm.DB { return r.db } // 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 { UserID uint LoggedIn bool // 是否已登录(false 时仅返回公开笔记,跨租户展示) 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.LoggedIn { // 登录用户:数据隔离,只看自己的 query = query.Where("user_id = ?", q.UserID) } else { // 游客:只看所有公开无密码的笔记(跨租户展示) query = query.Where("is_public = ?", true).Where("(password IS NULL OR password = '')") } 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(userID uint) ([]model.NoteListItem, error) { var items []model.NoteListItem err := r.db.Model(&model.Note{}). Where("user_id = ?", userID). 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(userID uint, loggedIn bool) ([]model.NoteListItem, error) { var items []model.NoteListItem q := r.db.Model(&model.Note{}) if loggedIn { // 登录用户:展示自己名下所有未删除笔记(含私有,用于其个人笔记空间) q = q.Where("user_id = ?", userID) } else { // 游客:仅展示跨租户的全部公开无密码笔记 q = q.Where("is_public = ?", true).Where("(password IS NULL OR password = '')") } err := q. 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(userID, parentID uint) ([]model.NoteListItem, error) { var items []model.NoteListItem err := r.db.Model(&model.Note{}). Where("user_id = ?", userID). 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(userID, parentID uint) (int64, error) { var count int64 err := r.db.Model(&model.Note{}).Where("user_id = ?", userID).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(userID uint) ([]model.NoteListItem, error) { var items []model.NoteListItem err := r.db.Unscoped().Model(&model.Note{}). Where("user_id = ?", userID). 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(userID uint, 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("user_id = ?", userID). 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 } // SearchPublic 游客搜索(仅搜索所有公开无密码笔记) func (r *NoteRepository) SearchPublic(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("is_public = ?", true). Where("(password IS NULL OR password = '')"). 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(userID uint) ([]string, error) { var categories []string err := r.db.Model(&model.Note{}). Where("user_id = ?", userID). Distinct("category"). Where("category != '' AND is_folder = ?", false). Pluck("category", &categories).Error return categories, err } // GetTags 获取所有标签(排除已删除和目录,按用户隔离) func (r *NoteRepository) GetTags(userID uint) ([]string, error) { var tagsJSON []string err := r.db.Model(&model.Note{}). Where("user_id = ?", userID). 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 } // ─────────────── 标签管理 ─────────────── // GetPublicCategories 游客获取所有公开笔记的分类 func (r *NoteRepository) GetPublicCategories() ([]string, error) { var categories []string err := r.db.Model(&model.Note{}). Where("is_public = ?", true). Distinct("category"). Where("category != '' AND is_folder = ?", false). Pluck("category", &categories).Error return categories, err } // GetPublicTags 游客获取所有公开笔记的标签 func (r *NoteRepository) GetPublicTags() ([]string, error) { var tagsJSON []string err := r.db.Model(&model.Note{}). Where("is_public = ?", true). Where("tags != '' AND tags IS NOT NULL AND is_folder = ?", false). Pluck("tags", &tagsJSON).Error if err != nil { return nil, err } return dedupeTags(tagsJSON), nil } // dedupeTags 去重并拼接标签 JSON 为字符串切片 func dedupeTags(tagsJSON []string) []string { seen := make(map[string]bool) var result []string for _, t := range tagsJSON { if !seen[t] { seen[t] = true result = append(result, t) } } return result } // UpdateTagAll 将所有笔记中出现的指定标签重命名/合并/删除(按用户隔离)。 // oldTag 为要操作的旧标签;newTag 传入新名称实现重命名,传空字符串则删除该标签。 func (r *NoteRepository) UpdateTagAll(userID uint, oldTag, newTag string) (int64, error) { var notes []model.Note if err := r.db.Where("user_id = ?", userID). 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(userID uint) ([]model.NoteListItem, error) { var items []model.NoteListItem err := r.db.Model(&model.Note{}). Where("user_id = ?", userID). Where("is_folder = ?", false). Select("id, title, tags").Find(&items).Error return items, err } // GetAllLinks 获取所有未删除笔记的 id、标题、内容(用于扫描双向链接与构建图谱,按用户隔离) // 仅返回轻量字段以降低内存占用 func (r *NoteRepository) GetAllContentLight(userID uint) ([]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("user_id = ?", userID). Where("is_folder = ?", false). Select("id, title, content").Find(&items).Error return items, err } // GetByIDs 批量获取笔记(用于解析链接指向的笔记是否存在,按用户隔离) func (r *NoteRepository) GetByIDs(userID uint, ids []uint) ([]model.NoteListItem, error) { if len(ids) == 0 { return nil, nil } var items []model.NoteListItem err := r.db.Model(&model.Note{}). Where("user_id = ?", userID). 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 }