Files
gochat/internal/repository/article_repo.go
T
2026-06-04 15:44:48 +08:00

242 lines
7.9 KiB
Go

package repository
import (
"context"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/search"
"gorm.io/gorm"
)
// ArticleRepo provides data access for Article.
type ArticleRepo struct {
db *gorm.DB
}
func NewArticleRepo(db *gorm.DB) *ArticleRepo {
return &ArticleRepo{db: db}
}
func (r *ArticleRepo) Create(ctx context.Context, article *model.Article) error {
return r.db.WithContext(ctx).Create(article).Error
}
func (r *ArticleRepo) GetByID(ctx context.Context, id uint) (*model.Article, error) {
var article model.Article
if err := r.db.WithContext(ctx).Preload("Category").Preload("Folder").Preload("Author").First(&article, id).Error; err != nil {
return nil, err
}
return &article, nil
}
func (r *ArticleRepo) Update(ctx context.Context, article *model.Article) error {
return r.db.WithContext(ctx).Save(article).Error
}
func (r *ArticleRepo) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.Article{}, id).Error
}
// FindByPortalID returns articles for a portal with pagination.
func (r *ArticleRepo) FindByPortalID(ctx context.Context, portalID uint, offset, limit int) ([]model.Article, int64, error) {
var articles []model.Article
var count int64
db := r.db.WithContext(ctx).Model(&model.Article{}).Where("portal_id = ?", portalID)
db.Count(&count)
if err := db.Offset(offset).Limit(limit).Order("position ASC").Find(&articles).Error; err != nil {
return nil, 0, err
}
return articles, count, nil
}
// FindByCategoryID returns articles for a category with pagination.
func (r *ArticleRepo) FindByCategoryID(ctx context.Context, categoryID uint, offset, limit int) ([]model.Article, int64, error) {
var articles []model.Article
var count int64
db := r.db.WithContext(ctx).Model(&model.Article{}).Where("category_id = ?", categoryID)
db.Count(&count)
if err := db.Offset(offset).Limit(limit).Order("position ASC").Find(&articles).Error; err != nil {
return nil, 0, err
}
return articles, count, nil
}
// FindByFolderID returns articles for a folder with pagination.
func (r *ArticleRepo) FindByFolderID(ctx context.Context, folderID uint, offset, limit int) ([]model.Article, int64, error) {
var articles []model.Article
var count int64
db := r.db.WithContext(ctx).Model(&model.Article{}).Where("folder_id = ?", folderID)
db.Count(&count)
if err := db.Offset(offset).Limit(limit).Order("position ASC").Find(&articles).Error; err != nil {
return nil, 0, err
}
return articles, count, nil
}
// FindByStatus returns articles by status for a portal with pagination.
func (r *ArticleRepo) FindByStatus(ctx context.Context, portalID uint, status model.ArticleStatus, offset, limit int) ([]model.Article, int64, error) {
var articles []model.Article
var count int64
db := r.db.WithContext(ctx).Model(&model.Article{}).Where("portal_id = ? AND status = ?", portalID, status)
db.Count(&count)
if err := db.Offset(offset).Limit(limit).Order("updated_at DESC").Find(&articles).Error; err != nil {
return nil, 0, err
}
return articles, count, nil
}
// FindBySlug returns an article by its slug.
func (r *ArticleRepo) FindBySlug(ctx context.Context, slug string) (*model.Article, error) {
var article model.Article
if err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&article).Error; err != nil {
return nil, err
}
return &article, nil
}
// ArticleSearchParams holds all filtering parameters for article search.
type ArticleSearchParams struct {
PortalID uint
Query string // full-text search term
SearchMode search.SearchMode // ilike (default) or trigram (pg_trgm fuzzy)
CategorySlug string // filter by category slug
Locale string // filter by locale
AuthorID *uint // filter by author
Status string // filter by status
SortBy string // "newest", "popular", "position"
Offset int
Limit int
}
// Search returns filtered + searched articles with pagination.
// Implements Article.search(params) from M9 spec: combines query + category/locale/author/status filters.
func (r *ArticleRepo) Search(ctx context.Context, params ArticleSearchParams) ([]model.Article, int64, error) {
var articles []model.Article
var count int64
db := r.db.WithContext(ctx).Model(&model.Article{}).Where("portal_id = ?", params.PortalID)
// Filter by category slug — join categories table
if params.CategorySlug != "" {
db = db.Joins("LEFT JOIN categories ON categories.id = articles.category_id").
Where("categories.slug = ?", params.CategorySlug)
}
// Filter by locale
if params.Locale != "" {
db = db.Where("articles.locale = ?", params.Locale)
}
// Filter by author
if params.AuthorID != nil {
db = db.Where("articles.author_id = ?", *params.AuthorID)
}
// Filter by status
if params.Status != "" {
db = db.Where("articles.status = ?", params.Status)
}
// Full-text search (supports ILIKE and pg_trgm similarity modes)
if params.Query != "" {
if params.SearchMode == search.SearchModeTrigram {
// pg_trgm fuzzy match on article fields
db = db.Where("articles.title % ? OR articles.description % ? OR articles.content % ?",
params.Query, params.Query, params.Query)
} else {
// ILIKE substring match (default)
searchPattern := "%" + params.Query + "%"
db = db.Where("articles.title ILIKE ? OR articles.description ILIKE ? OR articles.content ILIKE ?",
searchPattern, searchPattern, searchPattern)
}
}
// Count before pagination
db.Count(&count)
// Sorting
switch params.SortBy {
case "newest":
db = db.Order("articles.updated_at DESC")
case "popular":
db = db.Order("articles.views DESC")
default:
// When category is present, sort by position; otherwise by updated_at
if params.CategorySlug != "" || params.Status != "" {
db = db.Order("articles.position ASC")
} else {
db = db.Order("articles.updated_at DESC")
}
}
if err := db.Offset(params.Offset).Limit(params.Limit).Find(&articles).Error; err != nil {
return nil, 0, err
}
return articles, count, nil
}
// StatusCounts returns the count of articles by status for a portal.
// Implements ArticlesController#set_article_count from M9 spec.
func (r *ArticleRepo) StatusCounts(ctx context.Context, portalID uint) (map[string]int64, error) {
type statusCount struct {
Status string
Count int64
}
var results []statusCount
err := r.db.WithContext(ctx).Model(&model.Article{}).
Select("status, COUNT(*) as count").
Where("portal_id = ?", portalID).
Group("status").
Find(&results).Error
if err != nil {
return nil, err
}
counts := map[string]int64{
"all_count": 0,
"mine_count": 0,
"published_count": 0,
"draft_count": 0,
"archived_count": 0,
}
var total int64
for _, r := range results {
counts[r.Status+"_count"] = r.Count
total += r.Count
}
counts["all_count"] = total
return counts, nil
}
// UpdatePositions batch-updates article positions for ordering.
// Implements Article.update_positions from M9 spec.
func (r *ArticleRepo) UpdatePositions(ctx context.Context, positions map[uint]int) error {
for id, pos := range positions {
if err := r.db.WithContext(ctx).Model(&model.Article{}).Where("id = ?", id).Update("position", pos).Error; err != nil {
return err
}
}
return nil
}
// BulkUpdateStatus updates status for multiple articles.
func (r *ArticleRepo) BulkUpdateStatus(ctx context.Context, ids []uint, status string) error {
return r.db.WithContext(ctx).Model(&model.Article{}).
Where("id IN ?", ids).
Update("status", status).Error
}
// BulkDelete deletes multiple articles.
func (r *ArticleRepo) BulkDelete(ctx context.Context, ids []uint) error {
return r.db.WithContext(ctx).Where("id IN ?", ids).Delete(&model.Article{}).Error
}
// IncrementViews increments the view count for an article by 1.
func (r *ArticleRepo) IncrementViews(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Model(&model.Article{}).
Where("id = ?", id).
UpdateColumn("views", gorm.Expr("views + 1")).Error
}