Files
go_blog/models/tag.go
T
kevinandClaude Fable 5 2bdc368399 feat: implement tag management and search functionality
Features:
- Tag system with multi-language support (Chinese/English)
- Auto-create tags when associating with articles
- Tag filtering on homepage with visual indicators
- Full-text search across title, summary, and content
- Tag cloud sidebar showing article counts
- Search page with results display

Technical Implementation:
- Database models: Tag, ArticleTag (many-to-many)
- Tag count caching for performance
- Automatic tag slug generation
- Responsive design with mobile support
- I18n support for all new UI elements

Bug Fix:
- Fixed SQL column ambiguity error in tag filtering
- Added table prefix to ORDER BY clause in publishedArticleOrder

Routes:
- GET /search - Search results page
- GET /?tag=<slug> - Filter articles by tag
- GET /api/articles?tag=<slug> - API with tag filter support

Templates:
- Added tag input field to article create/edit form
- Added search box to homepage header
- Added tag cloud sidebar on homepage and search page
- Created new search results page template

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-22 20:59:32 +08:00

127 lines
3.0 KiB
Go

package models
import (
"strings"
"time"
"gorm.io/gorm"
)
// Tag represents a blog post tag with multi-language support.
type Tag struct {
ID uint `gorm:"primarykey" json:"id"`
NameZh string `gorm:"size:50;not null" json:"name_zh"`
NameEn string `gorm:"size:50;not null" json:"name_en"`
Slug string `gorm:"size:100;uniqueIndex;not null" json:"slug"`
Count int `gorm:"default:0" json:"count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the default GORM table name.
func (Tag) TableName() string {
return "tags"
}
// Name returns the tag name for the specified language.
func (t *Tag) Name(lang string) string {
if lang == "zh" {
return t.NameZh
}
return t.NameEn
}
// generateTagSlug creates a URL-friendly slug from tag name.
func generateTagSlug(name string) string {
slug := strings.ToLower(strings.TrimSpace(name))
slug = strings.ReplaceAll(slug, " ", "-")
slug = strings.ReplaceAll(slug, "_", "-")
return slug
}
// FindOrCreateTag finds a tag by name or creates it if it doesn't exist.
// If both nameZh and nameEn are provided, it uses them; otherwise uses the same name for both languages.
func FindOrCreateTag(db *gorm.DB, nameZh, nameEn string) (*Tag, error) {
nameZh = strings.TrimSpace(nameZh)
nameEn = strings.TrimSpace(nameEn)
// If only one name is provided, use it for both languages
if nameZh == "" && nameEn != "" {
nameZh = nameEn
} else if nameEn == "" && nameZh != "" {
nameEn = nameZh
}
if nameZh == "" {
return nil, nil
}
slug := generateTagSlug(nameZh)
var tag Tag
err := db.Where("slug = ?", slug).First(&tag).Error
if err == nil {
return &tag, nil
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
// Create new tag
tag = Tag{
NameZh: nameZh,
NameEn: nameEn,
Slug: slug,
Count: 0,
}
if err := db.Create(&tag).Error; err != nil {
return nil, err
}
return &tag, nil
}
// GetAllTags returns all tags ordered by count descending.
func GetAllTags(db *gorm.DB) ([]Tag, error) {
var tags []Tag
err := db.Order("count DESC, name_zh ASC").Find(&tags).Error
return tags, err
}
// GetTagBySlug returns a tag by its slug.
func GetTagBySlug(db *gorm.DB, slug string) (*Tag, error) {
var tag Tag
err := db.Where("slug = ?", slug).First(&tag).Error
if err != nil {
return nil, err
}
return &tag, nil
}
// UpdateTagCount recalculates the article count for a tag.
func UpdateTagCount(db *gorm.DB, tagID uint) error {
var count int64
db.Table("article_tags").Where("tag_id = ?", tagID).Count(&count)
return db.Model(&Tag{}).Where("id = ?", tagID).Update("count", count).Error
}
// UpdateAllTagCounts recalculates article counts for all tags.
func UpdateAllTagCounts(db *gorm.DB) error {
// Get all tags
var tags []Tag
if err := db.Find(&tags).Error; err != nil {
return err
}
// Update count for each tag
for _, tag := range tags {
if err := UpdateTagCount(db, tag.ID); err != nil {
return err
}
}
return nil
}