From f216ef4d1b8be51c85ef15b5b753a46b0887f344 Mon Sep 17 00:00:00 2001 From: Zhang Chao Date: Sat, 29 Aug 2026 01:40:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=86=85=E5=AE=B9=E5=8F=AF?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=B1=8F=E8=94=BD?= =?UTF-8?q?=E5=90=84=E7=A7=8D=E6=A0=87=E8=AE=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++ handlers/backend/post.go | 20 ++++----- handlers/frontend/view.go | 2 +- models/models.go | 49 +++++++++++----------- utils/markdown.go | 85 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 0d394c5..5ee4849 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ go.work.sum # vendor 目录(如使用 go mod 则无需提交) vendor/ + +# 命令行工具 +cmd/ diff --git a/handlers/backend/post.go b/handlers/backend/post.go index abcb052..fd25354 100644 --- a/handlers/backend/post.go +++ b/handlers/backend/post.go @@ -199,15 +199,16 @@ func CreatePost(c *gin.Context) { userID, _ := c.Get("userID") post := models.Post{ - Title: req.Title, - Slug: generateSlug(req.Title), - Content: req.Content, - Summary: req.Summary, - Cover: req.Cover, - AuthorID: userID.(uint), - CategoryID: req.CategoryID, - Status: req.Status, - IsTop: req.IsTop && isAdmin(c), // 非管理员不允许置顶 + Title: req.Title, + Slug: generateSlug(req.Title), + Content: req.Content, + SearchableText: string(utils.GenerateSearchableText(req.Content)), + Summary: req.Summary, + Cover: req.Cover, + AuthorID: userID.(uint), + CategoryID: req.CategoryID, + Status: req.Status, + IsTop: req.IsTop && isAdmin(c), // 非管理员不允许置顶 } if req.Status == "published" { @@ -269,6 +270,7 @@ func UpdatePost(c *gin.Context) { } if req.Content != "" { updates["content"] = req.Content + updates["searchable_text"] = string(utils.GenerateSearchableText(req.Content)) } if req.Summary != "" { updates["summary"] = req.Summary diff --git a/handlers/frontend/view.go b/handlers/frontend/view.go index b18f78a..bc7b42a 100644 --- a/handlers/frontend/view.go +++ b/handlers/frontend/view.go @@ -447,7 +447,7 @@ func SearchView(c *gin.Context) { db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author"). Where("status = ?", "published"). - Where("title LIKE ? OR content LIKE ? OR summary LIKE ?", + Where("title LIKE ? OR searchable_text LIKE ? OR summary LIKE ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") var total int64 diff --git a/models/models.go b/models/models.go index 555a2ff..046ae60 100644 --- a/models/models.go +++ b/models/models.go @@ -71,30 +71,31 @@ func (u *User) RecordLogin(db *gorm.DB) error { // ==================== 文章模型 ==================== type Post struct { - ID uint `gorm:"primaryKey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Title string `gorm:"size:200;not null" json:"title"` - Slug string `gorm:"uniqueIndex;size:200" json:"slug"` - Content string `gorm:"type:text" json:"content"` - Summary string `gorm:"size:500" json:"summary"` - Cover string `gorm:"size:255" json:"cover"` - AuthorID uint `gorm:"index" json:"author_id"` // 增加索引 - Author User `gorm:"foreignKey:AuthorID" json:"author"` - CategoryID uint `gorm:"index" json:"category_id"` // 增加索引 - Category Category `gorm:"foreignKey:CategoryID" json:"category"` - Tags []Tag `gorm:"many2many:post_tags;" json:"tags"` - Files []File `gorm:"many2many:post_files;" json:"files,omitempty"` // 关联文件 - Views int `gorm:"default:0" json:"views"` - Status string `gorm:"index;size:20;default:'draft'" json:"status"` // 增加索引 - IsTop bool `gorm:"default:false" json:"is_top"` - PublishedAt *time.Time `json:"published_at"` - Password string `gorm:"size:255" json:"password"` // 访问密码保护 - AllowComment bool `gorm:"default:true" json:"allow_comment"` // 是否允许评论 - AllowPing bool `gorm:"default:true" json:"allow_ping"` // 是否允许Pingback - Template string `gorm:"size:100" json:"template"` // 自定义模板文件 - Comments []Comment `json:"comments,omitempty"` + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Title string `gorm:"size:200;not null" json:"title"` + Slug string `gorm:"uniqueIndex;size:200" json:"slug"` + Content string `gorm:"type:text" json:"content"` + SearchableText string `gorm:"size:255" json:"searchable_text"` + Summary string `gorm:"size:500" json:"summary"` + Cover string `gorm:"size:255" json:"cover"` + AuthorID uint `gorm:"index" json:"author_id"` // 增加索引 + Author User `gorm:"foreignKey:AuthorID" json:"author"` + CategoryID uint `gorm:"index" json:"category_id"` // 增加索引 + Category Category `gorm:"foreignKey:CategoryID" json:"category"` + Tags []Tag `gorm:"many2many:post_tags;" json:"tags"` + Files []File `gorm:"many2many:post_files;" json:"files,omitempty"` // 关联文件 + Views int `gorm:"default:0" json:"views"` + Status string `gorm:"index;size:20;default:'draft'" json:"status"` // 增加索引 + IsTop bool `gorm:"default:false" json:"is_top"` + PublishedAt *time.Time `json:"published_at"` + Password string `gorm:"size:255" json:"password"` // 访问密码保护 + AllowComment bool `gorm:"default:true" json:"allow_comment"` // 是否允许评论 + AllowPing bool `gorm:"default:true" json:"allow_ping"` // 是否允许Pingback + Template string `gorm:"size:100" json:"template"` // 自定义模板文件 + Comments []Comment `json:"comments,omitempty"` } // TableName 指定表名 diff --git a/utils/markdown.go b/utils/markdown.go index 8a5cd50..7314bcb 100644 --- a/utils/markdown.go +++ b/utils/markdown.go @@ -3,6 +3,8 @@ package utils import ( "bytes" "html/template" + "regexp" + "strings" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" @@ -29,3 +31,86 @@ func RenderMarkdown(source string) template.HTML { } return template.HTML(buf.String()) } + +// 全局预编译正则(保持不变,但移除冗余) +var ( + // 标题 + reHeading = regexp.MustCompile(`(?m)^\s*#{1,6}\s+`) + // 无序列表 + reUnorderedList = regexp.MustCompile(`(?m)^\s*[-*+]\s+`) + // 有序列表 + reOrderedList = regexp.MustCompile(`(?m)^\s*\d+[.)]\s+`) + // 块引用 + reBlockquote = regexp.MustCompile(`(?m)^\s*>\s+`) + // 水平线 + reHr = regexp.MustCompile(`(?m)^\s*[-*_]{3,}\s*$`) + // 脚注定义 + reFootnoteDef = regexp.MustCompile(`(?m)^\[\^.+?\]:\s.*$`) + // 引用定义 + reRefDef = regexp.MustCompile(`(?m)^\s*\[.+?\]:\s*(.*)$`) + // 图片 + reImage = regexp.MustCompile(`(?s)!\[.*?\]\(.*?\)`) + // 引用图片 + reRefImage = regexp.MustCompile(`!\[.*?\](\[.*?\])?`) + // 链接 + reLink = regexp.MustCompile(`(?s)\[(.*?)\]\(.*?\)`) + // 引用式链接 + reRefLink = regexp.MustCompile(`\[([^\s=\[\]/]+)\](\[.*?\])?`) + // 粗体 + reBoldStar = regexp.MustCompile(`(?s)\*\*(.*?)\*\*`) + // 斜体 + reItalicStar = regexp.MustCompile(`(?s)\*(.*?)\*`) + // 下划线粗体 + reBoldUnderscore = regexp.MustCompile(`(?s)_(.*?)_`) + // 下划线斜体 + reItalicUnderscore = regexp.MustCompile(`(?s)_(.*?)_`) + + // HTML 标签 + reHTMLTag = regexp.MustCompile(`<[^>]+>`) + // 多余空白 + reMultiSpace = regexp.MustCompile(`\s+`) +) + +// GenerateSearchableText 提取 Markdown 中的纯文本 +func GenerateSearchableText(raw string) string { + if raw == "" { + return "" + } + text := raw + + // 全角空格转半角 + text = strings.ReplaceAll(text, "\u3000", " ") + + // 移除块级标记(一次性替换多个) + text = reHr.ReplaceAllString(text, "") + text = reBlockquote.ReplaceAllString(text, "") + text = reUnorderedList.ReplaceAllString(text, "") + text = reOrderedList.ReplaceAllString(text, "") + text = reHeading.ReplaceAllString(text, "") + + // 移除引用定义 + text = reFootnoteDef.ReplaceAllString(text, "") + text = reRefDef.ReplaceAllString(text, "") + + // 移除图片 + text = reImage.ReplaceAllString(text, "") + text = reRefImage.ReplaceAllString(text, "") + + // 处理链接:保留可见文字(先引用式后内联) + text = reRefLink.ReplaceAllString(text, "$1") + text = reLink.ReplaceAllString(text, "$1") + + // 移除粗体 + text = reBoldStar.ReplaceAllString(text, "") + text = reBoldUnderscore.ReplaceAllString(text, "") + // 移除斜体 + text = reItalicStar.ReplaceAllString(text, "") + text = reItalicUnderscore.ReplaceAllString(text, "") + + // 移除 HTML 标签 + text = reHTMLTag.ReplaceAllString(text, "") + + // 压缩空白 + text = reMultiSpace.ReplaceAllString(text, " ") + return strings.TrimSpace(text) +}