This commit is contained in:
2026-04-21 22:36:48 +08:00
commit 488c695fdf
748 changed files with 266838 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
package repositories
import (
"context"
"fmt"
"strings"
"time"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
repoQueries "hyapi-server/internal/domains/article/repositories/queries"
"hyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormAnnouncementRepository GORM公告仓储实现
type GormAnnouncementRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.AnnouncementRepository = (*GormAnnouncementRepository)(nil)
// NewGormAnnouncementRepository 创建GORM公告仓储
func NewGormAnnouncementRepository(db *gorm.DB, logger *zap.Logger) *GormAnnouncementRepository {
return &GormAnnouncementRepository{
db: db,
logger: logger,
}
}
// Create 创建公告
func (r *GormAnnouncementRepository) Create(ctx context.Context, entity entities.Announcement) (entities.Announcement, error) {
r.logger.Info("创建公告", zap.String("id", entity.ID), zap.String("title", entity.Title))
err := r.db.WithContext(ctx).Create(&entity).Error
if err != nil {
r.logger.Error("创建公告失败", zap.Error(err))
return entity, err
}
return entity, nil
}
// GetByID 根据ID获取公告
func (r *GormAnnouncementRepository) GetByID(ctx context.Context, id string) (entities.Announcement, error) {
var entity entities.Announcement
err := r.db.WithContext(ctx).
Where("id = ?", id).
First(&entity).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return entity, fmt.Errorf("公告不存在")
}
r.logger.Error("获取公告失败", zap.String("id", id), zap.Error(err))
return entity, err
}
return entity, nil
}
// Update 更新公告
func (r *GormAnnouncementRepository) Update(ctx context.Context, entity entities.Announcement) error {
r.logger.Info("更新公告", zap.String("id", entity.ID))
err := r.db.WithContext(ctx).Save(&entity).Error
if err != nil {
r.logger.Error("更新公告失败", zap.String("id", entity.ID), zap.Error(err))
return err
}
return nil
}
// Delete 删除公告
func (r *GormAnnouncementRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除公告", zap.String("id", id))
err := r.db.WithContext(ctx).Delete(&entities.Announcement{}, "id = ?", id).Error
if err != nil {
r.logger.Error("删除公告失败", zap.String("id", id), zap.Error(err))
return err
}
return nil
}
// FindByStatus 根据状态查找公告
func (r *GormAnnouncementRepository) FindByStatus(ctx context.Context, status entities.AnnouncementStatus) ([]*entities.Announcement, error) {
var announcements []entities.Announcement
err := r.db.WithContext(ctx).
Where("status = ?", status).
Order("created_at DESC").
Find(&announcements).Error
if err != nil {
r.logger.Error("根据状态查找公告失败", zap.String("status", string(status)), zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Announcement, len(announcements))
for i := range announcements {
result[i] = &announcements[i]
}
return result, nil
}
// FindScheduled 查找定时发布的公告
func (r *GormAnnouncementRepository) FindScheduled(ctx context.Context) ([]*entities.Announcement, error) {
var announcements []entities.Announcement
now := time.Now()
err := r.db.WithContext(ctx).
Where("status = ? AND scheduled_at IS NOT NULL AND scheduled_at <= ?", entities.AnnouncementStatusDraft, now).
Order("scheduled_at ASC").
Find(&announcements).Error
if err != nil {
r.logger.Error("查找定时发布公告失败", zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Announcement, len(announcements))
for i := range announcements {
result[i] = &announcements[i]
}
return result, nil
}
// ListAnnouncements 获取公告列表
func (r *GormAnnouncementRepository) ListAnnouncements(ctx context.Context, query *repoQueries.ListAnnouncementQuery) ([]*entities.Announcement, int64, error) {
var announcements []entities.Announcement
var total int64
dbQuery := r.db.WithContext(ctx).Model(&entities.Announcement{})
// 应用筛选条件
if query.Status != "" {
dbQuery = dbQuery.Where("status = ?", query.Status)
}
if query.Title != "" {
dbQuery = dbQuery.Where("title ILIKE ?", "%"+query.Title+"%")
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
r.logger.Error("获取公告列表总数失败", zap.Error(err))
return nil, 0, err
}
// 应用排序
if query.OrderBy != "" {
orderDir := "DESC"
if query.OrderDir != "" {
orderDir = strings.ToUpper(query.OrderDir)
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", query.OrderBy, orderDir))
} else {
dbQuery = dbQuery.Order("created_at DESC")
}
// 应用分页
if query.Page > 0 && query.PageSize > 0 {
offset := (query.Page - 1) * query.PageSize
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
}
// 获取数据
if err := dbQuery.Find(&announcements).Error; err != nil {
r.logger.Error("获取公告列表失败", zap.Error(err))
return nil, 0, err
}
// 转换为指针切片
result := make([]*entities.Announcement, len(announcements))
for i := range announcements {
result[i] = &announcements[i]
}
return result, total, nil
}
// CountByStatus 根据状态统计公告数量
func (r *GormAnnouncementRepository) CountByStatus(ctx context.Context, status entities.AnnouncementStatus) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Announcement{}).
Where("status = ?", status).
Count(&count).Error
if err != nil {
r.logger.Error("统计公告数量失败", zap.String("status", string(status)), zap.Error(err))
return 0, err
}
return count, nil
}
// UpdateStatistics 更新统计信息
// 注意:公告实体目前没有统计字段,此方法预留扩展
func (r *GormAnnouncementRepository) UpdateStatistics(ctx context.Context, announcementID string) error {
r.logger.Info("更新公告统计信息", zap.String("announcement_id", announcementID))
// TODO: 如果将来需要统计字段(如阅读量等),可以在这里实现
return nil
}
// ================ 实现 BaseRepository 接口的其他方法 ================
// Count 统计数量
func (r *GormAnnouncementRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
dbQuery := r.db.WithContext(ctx).Model(&entities.Announcement{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("title LIKE ? OR content LIKE ?", search, search)
}
var count int64
err := dbQuery.Count(&count).Error
return count, err
}
// Exists 检查是否存在
func (r *GormAnnouncementRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Announcement{}).
Where("id = ?", id).
Count(&count).Error
return count > 0, err
}
// SoftDelete 软删除
func (r *GormAnnouncementRepository) SoftDelete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entities.Announcement{}, "id = ?", id).Error
}
// Restore 恢复软删除
func (r *GormAnnouncementRepository) Restore(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Unscoped().Model(&entities.Announcement{}).
Where("id = ?", id).
Update("deleted_at", nil).Error
}
// CreateBatch 批量创建
func (r *GormAnnouncementRepository) CreateBatch(ctx context.Context, entities []entities.Announcement) error {
return r.db.WithContext(ctx).Create(&entities).Error
}
// GetByIDs 根据ID列表获取
func (r *GormAnnouncementRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Announcement, error) {
var announcements []entities.Announcement
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&announcements).Error
return announcements, err
}
// UpdateBatch 批量更新
func (r *GormAnnouncementRepository) UpdateBatch(ctx context.Context, entities []entities.Announcement) error {
return r.db.WithContext(ctx).Save(&entities).Error
}
// DeleteBatch 批量删除
func (r *GormAnnouncementRepository) DeleteBatch(ctx context.Context, ids []string) error {
return r.db.WithContext(ctx).Delete(&entities.Announcement{}, "id IN ?", ids).Error
}
// List 列表查询
func (r *GormAnnouncementRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Announcement, error) {
var announcements []entities.Announcement
dbQuery := r.db.WithContext(ctx).Model(&entities.Announcement{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("title LIKE ? OR content LIKE ?", search, search)
}
// 应用排序
if options.Sort != "" {
order := "DESC"
if options.Order != "" {
order = strings.ToUpper(options.Order)
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", options.Sort, order))
} else {
dbQuery = dbQuery.Order("created_at DESC")
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
dbQuery = dbQuery.Offset(offset).Limit(options.PageSize)
}
// 预加载关联数据
if len(options.Include) > 0 {
for _, include := range options.Include {
dbQuery = dbQuery.Preload(include)
}
}
err := dbQuery.Find(&announcements).Error
return announcements, err
}

View File

@@ -0,0 +1,592 @@
package repositories
import (
"context"
"fmt"
"strings"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
repoQueries "hyapi-server/internal/domains/article/repositories/queries"
"hyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormArticleRepository GORM文章仓储实现
type GormArticleRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.ArticleRepository = (*GormArticleRepository)(nil)
// NewGormArticleRepository 创建GORM文章仓储
func NewGormArticleRepository(db *gorm.DB, logger *zap.Logger) *GormArticleRepository {
return &GormArticleRepository{
db: db,
logger: logger,
}
}
// Create 创建文章
func (r *GormArticleRepository) Create(ctx context.Context, entity entities.Article) (entities.Article, error) {
r.logger.Info("创建文章", zap.String("id", entity.ID), zap.String("title", entity.Title))
err := r.db.WithContext(ctx).Create(&entity).Error
if err != nil {
r.logger.Error("创建文章失败", zap.Error(err))
return entity, err
}
return entity, nil
}
// GetByID 根据ID获取文章
func (r *GormArticleRepository) GetByID(ctx context.Context, id string) (entities.Article, error) {
var entity entities.Article
err := r.db.WithContext(ctx).
Preload("Category").
Preload("Tags").
Where("id = ?", id).
First(&entity).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return entity, fmt.Errorf("文章不存在")
}
r.logger.Error("获取文章失败", zap.String("id", id), zap.Error(err))
return entity, err
}
return entity, nil
}
// Update 更新文章
func (r *GormArticleRepository) Update(ctx context.Context, entity entities.Article) error {
r.logger.Info("更新文章", zap.String("id", entity.ID))
err := r.db.WithContext(ctx).Save(&entity).Error
if err != nil {
r.logger.Error("更新文章失败", zap.String("id", entity.ID), zap.Error(err))
return err
}
return nil
}
// Delete 删除文章
func (r *GormArticleRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除文章", zap.String("id", id))
err := r.db.WithContext(ctx).Delete(&entities.Article{}, "id = ?", id).Error
if err != nil {
r.logger.Error("删除文章失败", zap.String("id", id), zap.Error(err))
return err
}
return nil
}
// FindByAuthorID 根据作者ID查找文章
func (r *GormArticleRepository) FindByAuthorID(ctx context.Context, authorID string) ([]*entities.Article, error) {
var articles []entities.Article
err := r.db.WithContext(ctx).
Preload("Category").
Preload("Tags").
Where("author_id = ?", authorID).
Order("created_at DESC").
Find(&articles).Error
if err != nil {
r.logger.Error("根据作者ID查找文章失败", zap.String("author_id", authorID), zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Article, len(articles))
for i := range articles {
result[i] = &articles[i]
}
return result, nil
}
// FindByCategoryID 根据分类ID查找文章
func (r *GormArticleRepository) FindByCategoryID(ctx context.Context, categoryID string) ([]*entities.Article, error) {
var articles []entities.Article
err := r.db.WithContext(ctx).
Preload("Category").
Preload("Tags").
Where("category_id = ?", categoryID).
Order("created_at DESC").
Find(&articles).Error
if err != nil {
r.logger.Error("根据分类ID查找文章失败", zap.String("category_id", categoryID), zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Article, len(articles))
for i := range articles {
result[i] = &articles[i]
}
return result, nil
}
// FindByStatus 根据状态查找文章
func (r *GormArticleRepository) FindByStatus(ctx context.Context, status entities.ArticleStatus) ([]*entities.Article, error) {
var articles []entities.Article
err := r.db.WithContext(ctx).
Preload("Category").
Preload("Tags").
Where("status = ?", status).
Order("created_at DESC").
Find(&articles).Error
if err != nil {
r.logger.Error("根据状态查找文章失败", zap.String("status", string(status)), zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Article, len(articles))
for i := range articles {
result[i] = &articles[i]
}
return result, nil
}
// FindFeatured 查找推荐文章
func (r *GormArticleRepository) FindFeatured(ctx context.Context) ([]*entities.Article, error) {
var articles []entities.Article
err := r.db.WithContext(ctx).
Preload("Category").
Preload("Tags").
Where("is_featured = ? AND status = ?", true, entities.ArticleStatusPublished).
Order("published_at DESC").
Find(&articles).Error
if err != nil {
r.logger.Error("查找推荐文章失败", zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Article, len(articles))
for i := range articles {
result[i] = &articles[i]
}
return result, nil
}
// Search 搜索文章
func (r *GormArticleRepository) Search(ctx context.Context, query *repoQueries.SearchArticleQuery) ([]*entities.Article, int64, error) {
var articles []entities.Article
var total int64
dbQuery := r.db.WithContext(ctx).Model(&entities.Article{})
// 应用搜索条件
if query.Keyword != "" {
keyword := "%" + query.Keyword + "%"
dbQuery = dbQuery.Where("title LIKE ? OR content LIKE ? OR summary LIKE ?", keyword, keyword, keyword)
}
if query.CategoryID != "" {
// 如果指定了分类ID只查询该分类的文章包括没有分类的文章当CategoryID为空字符串时
if query.CategoryID == "null" || query.CategoryID == "" {
// 查询没有分类的文章
dbQuery = dbQuery.Where("category_id IS NULL OR category_id = ''")
} else {
// 查询指定分类的文章
dbQuery = dbQuery.Where("category_id = ?", query.CategoryID)
}
}
if query.AuthorID != "" {
dbQuery = dbQuery.Where("author_id = ?", query.AuthorID)
}
if query.Status != "" {
dbQuery = dbQuery.Where("status = ?", query.Status)
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
r.logger.Error("获取搜索结果总数失败", zap.Error(err))
return nil, 0, err
}
// 应用排序
if query.OrderBy != "" {
orderDir := "DESC"
if query.OrderDir != "" {
orderDir = strings.ToUpper(query.OrderDir)
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", query.OrderBy, orderDir))
} else {
dbQuery = dbQuery.Order("created_at DESC")
}
// 应用分页
if query.Page > 0 && query.PageSize > 0 {
offset := (query.Page - 1) * query.PageSize
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
}
// 预加载关联数据
dbQuery = dbQuery.Preload("Category").Preload("Tags")
// 获取数据
if err := dbQuery.Find(&articles).Error; err != nil {
r.logger.Error("搜索文章失败", zap.Error(err))
return nil, 0, err
}
// 转换为指针切片
result := make([]*entities.Article, len(articles))
for i := range articles {
result[i] = &articles[i]
}
return result, total, nil
}
// ListArticles 获取文章列表(用户端)
func (r *GormArticleRepository) ListArticles(ctx context.Context, query *repoQueries.ListArticleQuery) ([]*entities.Article, int64, error) {
var articles []entities.Article
var total int64
dbQuery := r.db.WithContext(ctx).Model(&entities.Article{})
// 用户端不显示归档文章
dbQuery = dbQuery.Where("status != ?", entities.ArticleStatusArchived)
// 应用筛选条件
if query.Status != "" {
dbQuery = dbQuery.Where("status = ?", query.Status)
}
if query.CategoryID != "" {
// 如果指定了分类ID只查询该分类的文章包括没有分类的文章当CategoryID为空字符串时
if query.CategoryID == "null" || query.CategoryID == "" {
// 查询没有分类的文章
dbQuery = dbQuery.Where("category_id IS NULL OR category_id = ''")
} else {
// 查询指定分类的文章
dbQuery = dbQuery.Where("category_id = ?", query.CategoryID)
}
}
if query.TagID != "" {
// 如果指定了标签ID只查询有关联该标签的文章
// 使用子查询而不是JOIN避免影响其他查询条件
subQuery := r.db.WithContext(ctx).Table("article_tag_relations").
Select("article_id").
Where("tag_id = ?", query.TagID)
dbQuery = dbQuery.Where("id IN (?)", subQuery)
}
if query.Title != "" {
dbQuery = dbQuery.Where("title ILIKE ?", "%"+query.Title+"%")
}
if query.Summary != "" {
dbQuery = dbQuery.Where("summary ILIKE ?", "%"+query.Summary+"%")
}
if query.IsFeatured != nil {
dbQuery = dbQuery.Where("is_featured = ?", *query.IsFeatured)
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
r.logger.Error("获取文章列表总数失败", zap.Error(err))
return nil, 0, err
}
// 应用排序
if query.OrderBy != "" {
orderDir := "DESC"
if query.OrderDir != "" {
orderDir = strings.ToUpper(query.OrderDir)
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", query.OrderBy, orderDir))
} else {
dbQuery = dbQuery.Order("created_at DESC")
}
// 应用分页
if query.Page > 0 && query.PageSize > 0 {
offset := (query.Page - 1) * query.PageSize
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
}
// 预加载关联数据
dbQuery = dbQuery.Preload("Category").Preload("Tags")
// 获取数据
if err := dbQuery.Find(&articles).Error; err != nil {
r.logger.Error("获取文章列表失败", zap.Error(err))
return nil, 0, err
}
// 转换为指针切片
result := make([]*entities.Article, len(articles))
for i := range articles {
result[i] = &articles[i]
}
return result, total, nil
}
// ListArticlesForAdmin 获取文章列表(管理员端)
func (r *GormArticleRepository) ListArticlesForAdmin(ctx context.Context, query *repoQueries.ListArticleQuery) ([]*entities.Article, int64, error) {
var articles []entities.Article
var total int64
dbQuery := r.db.WithContext(ctx).Model(&entities.Article{})
// 应用筛选条件
if query.Status != "" {
dbQuery = dbQuery.Where("status = ?", query.Status)
}
if query.CategoryID != "" {
// 如果指定了分类ID只查询该分类的文章包括没有分类的文章当CategoryID为空字符串时
if query.CategoryID == "null" || query.CategoryID == "" {
// 查询没有分类的文章
dbQuery = dbQuery.Where("category_id IS NULL OR category_id = ''")
} else {
// 查询指定分类的文章
dbQuery = dbQuery.Where("category_id = ?", query.CategoryID)
}
}
if query.TagID != "" {
// 如果指定了标签ID只查询有关联该标签的文章
// 使用子查询而不是JOIN避免影响其他查询条件
subQuery := r.db.WithContext(ctx).Table("article_tag_relations").
Select("article_id").
Where("tag_id = ?", query.TagID)
dbQuery = dbQuery.Where("id IN (?)", subQuery)
}
if query.Title != "" {
dbQuery = dbQuery.Where("title ILIKE ?", "%"+query.Title+"%")
}
if query.Summary != "" {
dbQuery = dbQuery.Where("summary ILIKE ?", "%"+query.Summary+"%")
}
if query.IsFeatured != nil {
dbQuery = dbQuery.Where("is_featured = ?", *query.IsFeatured)
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
r.logger.Error("获取文章列表总数失败", zap.Error(err))
return nil, 0, err
}
// 应用排序
if query.OrderBy != "" {
orderDir := "DESC"
if query.OrderDir != "" {
orderDir = strings.ToUpper(query.OrderDir)
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", query.OrderBy, orderDir))
} else {
dbQuery = dbQuery.Order("created_at DESC")
}
// 应用分页
if query.Page > 0 && query.PageSize > 0 {
offset := (query.Page - 1) * query.PageSize
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
}
// 预加载关联数据
dbQuery = dbQuery.Preload("Category").Preload("Tags")
// 获取数据
if err := dbQuery.Find(&articles).Error; err != nil {
r.logger.Error("获取文章列表失败", zap.Error(err))
return nil, 0, err
}
// 转换为指针切片
result := make([]*entities.Article, len(articles))
for i := range articles {
result[i] = &articles[i]
}
return result, total, nil
}
// CountByCategoryID 统计分类文章数量
func (r *GormArticleRepository) CountByCategoryID(ctx context.Context, categoryID string) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Article{}).
Where("category_id = ?", categoryID).
Count(&count).Error
if err != nil {
r.logger.Error("统计分类文章数量失败", zap.String("category_id", categoryID), zap.Error(err))
return 0, err
}
return count, nil
}
// CountByStatus 统计状态文章数量
func (r *GormArticleRepository) CountByStatus(ctx context.Context, status entities.ArticleStatus) (int64, error) {
var count int64
dbQuery := r.db.WithContext(ctx).Model(&entities.Article{})
if status != "" {
dbQuery = dbQuery.Where("status = ?", status)
}
err := dbQuery.Count(&count).Error
if err != nil {
r.logger.Error("统计状态文章数量失败", zap.String("status", string(status)), zap.Error(err))
return 0, err
}
return count, nil
}
// IncrementViewCount 增加阅读量
func (r *GormArticleRepository) IncrementViewCount(ctx context.Context, articleID string) error {
err := r.db.WithContext(ctx).Model(&entities.Article{}).
Where("id = ?", articleID).
UpdateColumn("view_count", gorm.Expr("view_count + ?", 1)).Error
if err != nil {
r.logger.Error("增加阅读量失败", zap.String("article_id", articleID), zap.Error(err))
return err
}
return nil
}
// 实现 BaseRepository 接口的其他方法
func (r *GormArticleRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
dbQuery := r.db.WithContext(ctx).Model(&entities.Article{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("title LIKE ? OR content LIKE ?", search, search)
}
var count int64
err := dbQuery.Count(&count).Error
return count, err
}
func (r *GormArticleRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Article{}).
Where("id = ?", id).
Count(&count).Error
return count > 0, err
}
func (r *GormArticleRepository) SoftDelete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entities.Article{}, "id = ?", id).Error
}
func (r *GormArticleRepository) Restore(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Unscoped().Model(&entities.Article{}).
Where("id = ?", id).
Update("deleted_at", nil).Error
}
func (r *GormArticleRepository) CreateBatch(ctx context.Context, entities []entities.Article) error {
return r.db.WithContext(ctx).Create(&entities).Error
}
func (r *GormArticleRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Article, error) {
var articles []entities.Article
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&articles).Error
return articles, err
}
func (r *GormArticleRepository) UpdateBatch(ctx context.Context, entities []entities.Article) error {
return r.db.WithContext(ctx).Save(&entities).Error
}
func (r *GormArticleRepository) DeleteBatch(ctx context.Context, ids []string) error {
return r.db.WithContext(ctx).Delete(&entities.Article{}, "id IN ?", ids).Error
}
func (r *GormArticleRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Article, error) {
var articles []entities.Article
dbQuery := r.db.WithContext(ctx).Model(&entities.Article{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("title LIKE ? OR content LIKE ?", search, search)
}
// 应用排序
if options.Sort != "" {
order := "DESC"
if options.Order != "" {
order = strings.ToUpper(options.Order)
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", options.Sort, order))
} else {
dbQuery = dbQuery.Order("created_at DESC")
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
dbQuery = dbQuery.Offset(offset).Limit(options.PageSize)
}
// 预加载关联数据
if len(options.Include) > 0 {
for _, include := range options.Include {
dbQuery = dbQuery.Preload(include)
}
}
err := dbQuery.Find(&articles).Error
return articles, err
}

View File

@@ -0,0 +1,247 @@
package repositories
import (
"context"
"fmt"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
"hyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormCategoryRepository GORM分类仓储实现
type GormCategoryRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.CategoryRepository = (*GormCategoryRepository)(nil)
// NewGormCategoryRepository 创建GORM分类仓储
func NewGormCategoryRepository(db *gorm.DB, logger *zap.Logger) *GormCategoryRepository {
return &GormCategoryRepository{
db: db,
logger: logger,
}
}
// Create 创建分类
func (r *GormCategoryRepository) Create(ctx context.Context, entity entities.Category) (entities.Category, error) {
r.logger.Info("创建分类", zap.String("id", entity.ID), zap.String("name", entity.Name))
err := r.db.WithContext(ctx).Create(&entity).Error
if err != nil {
r.logger.Error("创建分类失败", zap.Error(err))
return entity, err
}
return entity, nil
}
// GetByID 根据ID获取分类
func (r *GormCategoryRepository) GetByID(ctx context.Context, id string) (entities.Category, error) {
var entity entities.Category
err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return entity, fmt.Errorf("分类不存在")
}
r.logger.Error("获取分类失败", zap.String("id", id), zap.Error(err))
return entity, err
}
return entity, nil
}
// Update 更新分类
func (r *GormCategoryRepository) Update(ctx context.Context, entity entities.Category) error {
r.logger.Info("更新分类", zap.String("id", entity.ID))
err := r.db.WithContext(ctx).Save(&entity).Error
if err != nil {
r.logger.Error("更新分类失败", zap.String("id", entity.ID), zap.Error(err))
return err
}
return nil
}
// Delete 删除分类
func (r *GormCategoryRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除分类", zap.String("id", id))
err := r.db.WithContext(ctx).Delete(&entities.Category{}, "id = ?", id).Error
if err != nil {
r.logger.Error("删除分类失败", zap.String("id", id), zap.Error(err))
return err
}
return nil
}
// FindActive 查找启用的分类
func (r *GormCategoryRepository) FindActive(ctx context.Context) ([]*entities.Category, error) {
var categories []entities.Category
err := r.db.WithContext(ctx).
Where("active = ?", true).
Order("sort_order ASC, created_at ASC").
Find(&categories).Error
if err != nil {
r.logger.Error("查找启用分类失败", zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Category, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// FindBySortOrder 按排序查找分类
func (r *GormCategoryRepository) FindBySortOrder(ctx context.Context) ([]*entities.Category, error) {
var categories []entities.Category
err := r.db.WithContext(ctx).
Order("sort_order ASC, created_at ASC").
Find(&categories).Error
if err != nil {
r.logger.Error("按排序查找分类失败", zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Category, len(categories))
for i := range categories {
result[i] = &categories[i]
}
return result, nil
}
// CountActive 统计启用分类数量
func (r *GormCategoryRepository) CountActive(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Category{}).
Where("active = ?", true).
Count(&count).Error
if err != nil {
r.logger.Error("统计启用分类数量失败", zap.Error(err))
return 0, err
}
return count, nil
}
// 实现 BaseRepository 接口的其他方法
func (r *GormCategoryRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
dbQuery := r.db.WithContext(ctx).Model(&entities.Category{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("name LIKE ? OR description LIKE ?", search, search)
}
var count int64
err := dbQuery.Count(&count).Error
return count, err
}
func (r *GormCategoryRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Category{}).
Where("id = ?", id).
Count(&count).Error
return count > 0, err
}
func (r *GormCategoryRepository) SoftDelete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entities.Category{}, "id = ?", id).Error
}
func (r *GormCategoryRepository) Restore(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Unscoped().Model(&entities.Category{}).
Where("id = ?", id).
Update("deleted_at", nil).Error
}
func (r *GormCategoryRepository) CreateBatch(ctx context.Context, entities []entities.Category) error {
return r.db.WithContext(ctx).Create(&entities).Error
}
func (r *GormCategoryRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Category, error) {
var categories []entities.Category
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&categories).Error
return categories, err
}
func (r *GormCategoryRepository) UpdateBatch(ctx context.Context, entities []entities.Category) error {
return r.db.WithContext(ctx).Save(&entities).Error
}
func (r *GormCategoryRepository) DeleteBatch(ctx context.Context, ids []string) error {
return r.db.WithContext(ctx).Delete(&entities.Category{}, "id IN ?", ids).Error
}
func (r *GormCategoryRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Category, error) {
var categories []entities.Category
dbQuery := r.db.WithContext(ctx).Model(&entities.Category{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("name LIKE ? OR description LIKE ?", search, search)
}
// 应用排序
if options.Sort != "" {
order := "DESC"
if options.Order != "" {
order = options.Order
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", options.Sort, order))
} else {
dbQuery = dbQuery.Order("sort_order ASC, created_at ASC")
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
dbQuery = dbQuery.Offset(offset).Limit(options.PageSize)
}
// 预加载关联数据
if len(options.Include) > 0 {
for _, include := range options.Include {
dbQuery = dbQuery.Preload(include)
}
}
err := dbQuery.Find(&categories).Error
return categories, err
}

View File

@@ -0,0 +1,168 @@
package repositories
import (
"context"
"fmt"
"time"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormScheduledTaskRepository GORM定时任务仓储实现
type GormScheduledTaskRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.ScheduledTaskRepository = (*GormScheduledTaskRepository)(nil)
// NewGormScheduledTaskRepository 创建GORM定时任务仓储
func NewGormScheduledTaskRepository(db *gorm.DB, logger *zap.Logger) *GormScheduledTaskRepository {
return &GormScheduledTaskRepository{
db: db,
logger: logger,
}
}
// Create 创建定时任务记录
func (r *GormScheduledTaskRepository) Create(ctx context.Context, task entities.ScheduledTask) (entities.ScheduledTask, error) {
r.logger.Info("创建定时任务记录", zap.String("task_id", task.TaskID), zap.String("article_id", task.ArticleID))
err := r.db.WithContext(ctx).Create(&task).Error
if err != nil {
r.logger.Error("创建定时任务记录失败", zap.Error(err))
return task, err
}
return task, nil
}
// GetByTaskID 根据Asynq任务ID获取任务记录
func (r *GormScheduledTaskRepository) GetByTaskID(ctx context.Context, taskID string) (entities.ScheduledTask, error) {
var task entities.ScheduledTask
err := r.db.WithContext(ctx).
Preload("Article").
Where("task_id = ?", taskID).
First(&task).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return task, fmt.Errorf("定时任务不存在")
}
r.logger.Error("获取定时任务失败", zap.String("task_id", taskID), zap.Error(err))
return task, err
}
return task, nil
}
// GetByArticleID 根据文章ID获取任务记录
func (r *GormScheduledTaskRepository) GetByArticleID(ctx context.Context, articleID string) (entities.ScheduledTask, error) {
var task entities.ScheduledTask
err := r.db.WithContext(ctx).
Preload("Article").
Where("article_id = ? AND status IN (?)", articleID, []string{"pending", "running"}).
First(&task).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return task, fmt.Errorf("文章没有活动的定时任务")
}
r.logger.Error("获取文章定时任务失败", zap.String("article_id", articleID), zap.Error(err))
return task, err
}
return task, nil
}
// Update 更新任务记录
func (r *GormScheduledTaskRepository) Update(ctx context.Context, task entities.ScheduledTask) error {
r.logger.Info("更新定时任务记录", zap.String("task_id", task.TaskID), zap.String("status", string(task.Status)))
err := r.db.WithContext(ctx).Save(&task).Error
if err != nil {
r.logger.Error("更新定时任务记录失败", zap.String("task_id", task.TaskID), zap.Error(err))
return err
}
return nil
}
// Delete 删除任务记录
func (r *GormScheduledTaskRepository) Delete(ctx context.Context, taskID string) error {
r.logger.Info("删除定时任务记录", zap.String("task_id", taskID))
err := r.db.WithContext(ctx).Where("task_id = ?", taskID).Delete(&entities.ScheduledTask{}).Error
if err != nil {
r.logger.Error("删除定时任务记录失败", zap.String("task_id", taskID), zap.Error(err))
return err
}
return nil
}
// MarkAsCancelled 标记任务为已取消
func (r *GormScheduledTaskRepository) MarkAsCancelled(ctx context.Context, taskID string) error {
r.logger.Info("标记定时任务为已取消", zap.String("task_id", taskID))
result := r.db.WithContext(ctx).
Model(&entities.ScheduledTask{}).
Where("task_id = ? AND status IN (?)", taskID, []string{"pending", "running"}).
Updates(map[string]interface{}{
"status": entities.TaskStatusCancelled,
"completed_at": time.Now(),
})
if result.Error != nil {
r.logger.Error("标记定时任务为已取消失败", zap.String("task_id", taskID), zap.Error(result.Error))
return result.Error
}
if result.RowsAffected == 0 {
r.logger.Warn("没有找到需要取消的定时任务", zap.String("task_id", taskID))
}
return nil
}
// GetActiveTasks 获取活动状态的任务列表
func (r *GormScheduledTaskRepository) GetActiveTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
var tasks []entities.ScheduledTask
err := r.db.WithContext(ctx).
Preload("Article").
Where("status IN (?)", []string{"pending", "running"}).
Order("scheduled_at ASC").
Find(&tasks).Error
if err != nil {
r.logger.Error("获取活动定时任务列表失败", zap.Error(err))
return nil, err
}
return tasks, nil
}
// GetExpiredTasks 获取过期的任务列表
func (r *GormScheduledTaskRepository) GetExpiredTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
var tasks []entities.ScheduledTask
err := r.db.WithContext(ctx).
Preload("Article").
Where("status = ? AND scheduled_at < ?", entities.TaskStatusPending, time.Now()).
Order("scheduled_at ASC").
Find(&tasks).Error
if err != nil {
r.logger.Error("获取过期定时任务列表失败", zap.Error(err))
return nil, err
}
return tasks, nil
}

View File

@@ -0,0 +1,279 @@
package repositories
import (
"context"
"fmt"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
"hyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
// GormTagRepository GORM标签仓储实现
type GormTagRepository struct {
db *gorm.DB
logger *zap.Logger
}
// 编译时检查接口实现
var _ repositories.TagRepository = (*GormTagRepository)(nil)
// NewGormTagRepository 创建GORM标签仓储
func NewGormTagRepository(db *gorm.DB, logger *zap.Logger) *GormTagRepository {
return &GormTagRepository{
db: db,
logger: logger,
}
}
// Create 创建标签
func (r *GormTagRepository) Create(ctx context.Context, entity entities.Tag) (entities.Tag, error) {
r.logger.Info("创建标签", zap.String("id", entity.ID), zap.String("name", entity.Name))
err := r.db.WithContext(ctx).Create(&entity).Error
if err != nil {
r.logger.Error("创建标签失败", zap.Error(err))
return entity, err
}
return entity, nil
}
// GetByID 根据ID获取标签
func (r *GormTagRepository) GetByID(ctx context.Context, id string) (entities.Tag, error) {
var entity entities.Tag
err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return entity, fmt.Errorf("标签不存在")
}
r.logger.Error("获取标签失败", zap.String("id", id), zap.Error(err))
return entity, err
}
return entity, nil
}
// Update 更新标签
func (r *GormTagRepository) Update(ctx context.Context, entity entities.Tag) error {
r.logger.Info("更新标签", zap.String("id", entity.ID))
err := r.db.WithContext(ctx).Save(&entity).Error
if err != nil {
r.logger.Error("更新标签失败", zap.String("id", entity.ID), zap.Error(err))
return err
}
return nil
}
// Delete 删除标签
func (r *GormTagRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除标签", zap.String("id", id))
err := r.db.WithContext(ctx).Delete(&entities.Tag{}, "id = ?", id).Error
if err != nil {
r.logger.Error("删除标签失败", zap.String("id", id), zap.Error(err))
return err
}
return nil
}
// FindByArticleID 根据文章ID查找标签
func (r *GormTagRepository) FindByArticleID(ctx context.Context, articleID string) ([]*entities.Tag, error) {
var tags []entities.Tag
err := r.db.WithContext(ctx).
Joins("JOIN article_tag_relations ON article_tag_relations.tag_id = tags.id").
Where("article_tag_relations.article_id = ?", articleID).
Find(&tags).Error
if err != nil {
r.logger.Error("根据文章ID查找标签失败", zap.String("article_id", articleID), zap.Error(err))
return nil, err
}
// 转换为指针切片
result := make([]*entities.Tag, len(tags))
for i := range tags {
result[i] = &tags[i]
}
return result, nil
}
// FindByName 根据名称查找标签
func (r *GormTagRepository) FindByName(ctx context.Context, name string) (*entities.Tag, error) {
var tag entities.Tag
err := r.db.WithContext(ctx).Where("name = ?", name).First(&tag).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
r.logger.Error("根据名称查找标签失败", zap.String("name", name), zap.Error(err))
return nil, err
}
return &tag, nil
}
// AddTagToArticle 为文章添加标签
func (r *GormTagRepository) AddTagToArticle(ctx context.Context, articleID string, tagID string) error {
// 检查关联是否已存在
var count int64
err := r.db.WithContext(ctx).Table("article_tag_relations").
Where("article_id = ? AND tag_id = ?", articleID, tagID).
Count(&count).Error
if err != nil {
r.logger.Error("检查标签关联失败", zap.String("article_id", articleID), zap.String("tag_id", tagID), zap.Error(err))
return err
}
if count > 0 {
// 关联已存在,不需要重复添加
return nil
}
// 创建关联
err = r.db.WithContext(ctx).Exec(`
INSERT INTO article_tag_relations (article_id, tag_id)
VALUES (?, ?)
`, articleID, tagID).Error
if err != nil {
r.logger.Error("添加标签到文章失败", zap.String("article_id", articleID), zap.String("tag_id", tagID), zap.Error(err))
return err
}
r.logger.Info("添加标签到文章成功", zap.String("article_id", articleID), zap.String("tag_id", tagID))
return nil
}
// RemoveTagFromArticle 从文章移除标签
func (r *GormTagRepository) RemoveTagFromArticle(ctx context.Context, articleID string, tagID string) error {
err := r.db.WithContext(ctx).Exec(`
DELETE FROM article_tag_relations
WHERE article_id = ? AND tag_id = ?
`, articleID, tagID).Error
if err != nil {
r.logger.Error("从文章移除标签失败", zap.String("article_id", articleID), zap.String("tag_id", tagID), zap.Error(err))
return err
}
r.logger.Info("从文章移除标签成功", zap.String("article_id", articleID), zap.String("tag_id", tagID))
return nil
}
// GetArticleTags 获取文章的所有标签
func (r *GormTagRepository) GetArticleTags(ctx context.Context, articleID string) ([]*entities.Tag, error) {
return r.FindByArticleID(ctx, articleID)
}
// 实现 BaseRepository 接口的其他方法
func (r *GormTagRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
dbQuery := r.db.WithContext(ctx).Model(&entities.Tag{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("name LIKE ?", search)
}
var count int64
err := dbQuery.Count(&count).Error
return count, err
}
func (r *GormTagRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Tag{}).
Where("id = ?", id).
Count(&count).Error
return count > 0, err
}
func (r *GormTagRepository) SoftDelete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entities.Tag{}, "id = ?", id).Error
}
func (r *GormTagRepository) Restore(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Unscoped().Model(&entities.Tag{}).
Where("id = ?", id).
Update("deleted_at", nil).Error
}
func (r *GormTagRepository) CreateBatch(ctx context.Context, entities []entities.Tag) error {
return r.db.WithContext(ctx).Create(&entities).Error
}
func (r *GormTagRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Tag, error) {
var tags []entities.Tag
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&tags).Error
return tags, err
}
func (r *GormTagRepository) UpdateBatch(ctx context.Context, entities []entities.Tag) error {
return r.db.WithContext(ctx).Save(&entities).Error
}
func (r *GormTagRepository) DeleteBatch(ctx context.Context, ids []string) error {
return r.db.WithContext(ctx).Delete(&entities.Tag{}, "id IN ?", ids).Error
}
func (r *GormTagRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Tag, error) {
var tags []entities.Tag
dbQuery := r.db.WithContext(ctx).Model(&entities.Tag{})
// 应用筛选条件
if options.Filters != nil {
for key, value := range options.Filters {
dbQuery = dbQuery.Where(key+" = ?", value)
}
}
if options.Search != "" {
search := "%" + options.Search + "%"
dbQuery = dbQuery.Where("name LIKE ?", search)
}
// 应用排序
if options.Sort != "" {
order := "DESC"
if options.Order != "" {
order = options.Order
}
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", options.Sort, order))
} else {
dbQuery = dbQuery.Order("created_at ASC")
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
dbQuery = dbQuery.Offset(offset).Limit(options.PageSize)
}
// 预加载关联数据
if len(options.Include) > 0 {
for _, include := range options.Include {
dbQuery = dbQuery.Preload(include)
}
}
err := dbQuery.Find(&tags).Error
return tags, err
}