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>
127 lines
3.0 KiB
Go
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
|
|
}
|