feat: 分类系统 + 标签系统 — Phase 4 完成

分类:Model/Repo/Service/Controller + 管理后台树形页面 + 首页导航栏
标签:Model/Repo/Service/Controller + /tags/{slug} 落地页 + 写文章页输入
Post 加 CategoryID,Create/Update 支持分类和标签
SQL 迁移含 categories/tags/post_tags 表及预设未分类
This commit is contained in:
2026-06-22 00:30:29 +08:00
parent 977c52513a
commit 0b8f6f890d
30 changed files with 988 additions and 13 deletions

View File

@ -0,0 +1,63 @@
package repository
import (
"metazone.cc/mce/internal/model"
"metazone.cc/mce/internal/service"
"gorm.io/gorm"
)
// CategoryRepo 分类数据访问
type CategoryRepo struct {
db *gorm.DB
}
// NewCategoryRepo 构造函数
func NewCategoryRepo(db *gorm.DB) *CategoryRepo {
return &CategoryRepo{db: db}
}
// WithTx 基于事务创建新实例
func (r *CategoryRepo) WithTx(tx *gorm.DB) service.CategoryStore {
return &CategoryRepo{db: tx}
}
// Create 创建分类
func (r *CategoryRepo) Create(c *model.Category) error {
return r.db.Create(c).Error
}
// Update 更新分类
func (r *CategoryRepo) Update(c *model.Category) error {
return r.db.Save(c).Error
}
// Delete 删除分类
func (r *CategoryRepo) Delete(id uint) error {
return r.db.Delete(&model.Category{}, id).Error
}
// FindByID 按 ID 查询
func (r *CategoryRepo) FindByID(id uint) (*model.Category, error) {
var c model.Category
err := r.db.First(&c, id).Error
if err != nil {
return nil, err
}
return &c, nil
}
// ListAll 查询全部分类(按 sort 排序)
func (r *CategoryRepo) ListAll() ([]model.Category, error) {
var list []model.Category
err := r.db.Order("sort ASC, id ASC").Find(&list).Error
return list, err
}
// GetLeaves 查询所有叶子分类(有子分类的父节点不返回)
func (r *CategoryRepo) GetLeaves() ([]model.Category, error) {
var leaves []model.Category
err := r.db.Where("id NOT IN (SELECT DISTINCT parent_id FROM categories WHERE parent_id IS NOT NULL)").
Order("sort ASC, id ASC").Find(&leaves).Error
return leaves, err
}

View File

@ -0,0 +1,97 @@
package repository
import (
"metazone.cc/mce/internal/model"
"metazone.cc/mce/internal/service"
"gorm.io/gorm"
)
// TagRepo 标签数据访问
type TagRepo struct {
db *gorm.DB
}
// NewTagRepo 构造函数
func NewTagRepo(db *gorm.DB) *TagRepo {
return &TagRepo{db: db}
}
// WithTx 基于事务创建新实例
func (r *TagRepo) WithTx(tx *gorm.DB) service.TagStore {
return &TagRepo{db: tx}
}
// FindOrCreate 查找或创建标签(按名字)
func (r *TagRepo) FindOrCreate(name, slug string) (*model.Tag, error) {
var tag model.Tag
err := r.db.Where("slug = ?", slug).First(&tag).Error
if err == nil {
return &tag, nil
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
tag = model.Tag{Name: name, Slug: slug}
if err := r.db.Create(&tag).Error; err != nil {
return nil, err
}
return &tag, nil
}
// FindBySlug 按 slug 查询
func (r *TagRepo) FindBySlug(slug string) (*model.Tag, error) {
var tag model.Tag
err := r.db.Where("slug = ?", slug).First(&tag).Error
if err != nil {
return nil, err
}
return &tag, nil
}
// SetPostTags 设置文章的标签(删除旧的,插入新的)
func (r *TagRepo) SetPostTags(postID uint, tagIDs []uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("post_id = ?", postID).Delete(&model.PostTag{}).Error; err != nil {
return err
}
for _, tagID := range tagIDs {
if err := tx.Create(&model.PostTag{PostID: postID, TagID: tagID}).Error; err != nil {
return err
}
}
return nil
})
}
// GetPostTags 获取文章关联的标签
func (r *TagRepo) GetPostTags(postID uint) ([]model.Tag, error) {
var tags []model.Tag
err := r.db.Table("tags").
Joins("JOIN post_tags ON post_tags.tag_id = tags.id").
Where("post_tags.post_id = ?", postID).
Find(&tags).Error
return tags, err
}
// GetPostsByTag 按标签查询文章(公开+已发布)
func (r *TagRepo) GetPostsByTag(tagID uint, offset, limit int) ([]model.Post, int64, error) {
var total int64
query := r.db.Table("posts").
Joins("JOIN post_tags ON post_tags.post_id = posts.id").
Joins("LEFT JOIN users ON users.id = posts.user_id").
Where("post_tags.tag_id = ?", tagID).
Where("posts.status = ?", model.PostStatusApproved).
Where("posts.visibility = ?", model.VisibilityPublic).
Where("posts.deleted_at IS NULL").
Select("posts.*, users.username as author_name")
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var posts []model.Post
err := query.Order("CASE WHEN posts.pin_type = 'global' THEN 0 ELSE 1 END ASC, posts.pinned_at DESC NULLS LAST, posts.created_at DESC").
Offset(offset).Limit(limit).Find(&posts).Error
return posts, total, err
}