add article

This commit is contained in:
2025-09-01 18:29:59 +08:00
parent 34ff6ce916
commit 5d5372e359
59 changed files with 45435 additions and 1535 deletions

View File

@@ -171,9 +171,9 @@ func (r *RedisCache) GetMultiple(ctx context.Context, keys []string) (map[string
var data interface{}
// 修复改进JSON反序列化错误处理
if err := json.Unmarshal([]byte(val.(string)), &data); err != nil {
r.logger.Warn("反序列化缓存数据失败",
zap.String("key", keys[i]),
zap.String("value", val.(string)),
r.logger.Warn("反序列化缓存数据失败",
zap.String("key", keys[i]),
zap.String("value", val.(string)),
zap.Error(err))
continue
}
@@ -227,17 +227,18 @@ func (r *RedisCache) DeletePattern(ctx context.Context, pattern string) error {
} else {
fullPattern = r.getFullKey(pattern)
}
// 检查上下文是否已取消
if ctx.Err() != nil {
return ctx.Err()
}
var cursor uint64
var totalDeleted int64
maxIterations := 100 // 防止无限循环
iteration := 0
for {
// 检查迭代次数限制
iteration++
@@ -249,7 +250,7 @@ func (r *RedisCache) DeletePattern(ctx context.Context, pattern string) error {
)
break
}
// 检查上下文是否已取消
if ctx.Err() != nil {
r.logger.Warn("缓存删除操作被取消",
@@ -259,7 +260,7 @@ func (r *RedisCache) DeletePattern(ctx context.Context, pattern string) error {
)
return ctx.Err()
}
// 执行SCAN操作
keys, next, err := r.client.Scan(ctx, cursor, fullPattern, 1000).Result()
if err != nil {
@@ -272,27 +273,27 @@ func (r *RedisCache) DeletePattern(ctx context.Context, pattern string) error {
)
return err
}
r.logger.Error("扫描缓存键失败",
zap.String("pattern", fullPattern),
r.logger.Error("扫描缓存键失败",
zap.String("pattern", fullPattern),
zap.Error(err))
return err
}
// 批量删除找到的键
if len(keys) > 0 {
// 使用pipeline批量删除提高性能
pipe := r.client.Pipeline()
pipe.Del(ctx, keys...)
cmds, err := pipe.Exec(ctx)
if err != nil {
r.logger.Error("批量删除缓存键失败",
zap.Strings("keys", keys),
r.logger.Error("批量删除缓存键失败",
zap.Strings("keys", keys),
zap.Error(err))
return err
}
// 统计删除的键数量
for _, cmd := range cmds {
if delCmd, ok := cmd.(*redis.IntCmd); ok {
@@ -301,26 +302,26 @@ func (r *RedisCache) DeletePattern(ctx context.Context, pattern string) error {
}
}
}
r.logger.Debug("批量删除缓存键",
r.logger.Debug("批量删除缓存键",
zap.Strings("keys", keys),
zap.Int("batch_size", len(keys)),
zap.Int64("total_deleted", totalDeleted),
)
}
cursor = next
if cursor == 0 {
break
}
}
r.logger.Debug("缓存模式删除完成",
zap.String("pattern", fullPattern),
zap.Int64("total_deleted", totalDeleted),
zap.Int("iterations", iteration),
)
return nil
}

View File

@@ -0,0 +1,487 @@
package repositories
import (
"context"
"fmt"
"strings"
"tyapi-server/internal/domains/article/entities"
"tyapi-server/internal/domains/article/repositories"
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
"tyapi-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 != "" {
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{})
// 应用筛选条件
if query.Status != "" {
dbQuery = dbQuery.Where("status = ?", query.Status)
}
if query.CategoryID != "" {
dbQuery = dbQuery.Where("category_id = ?", query.CategoryID)
}
if query.TagID != "" {
// 通过标签关联表筛选
dbQuery = dbQuery.Joins("JOIN article_tag_relations ON articles.id = article_tag_relations.article_id").
Where("article_tag_relations.tag_id = ?", query.TagID)
}
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"
"tyapi-server/internal/domains/article/entities"
"tyapi-server/internal/domains/article/repositories"
"tyapi-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,279 @@
package repositories
import (
"context"
"fmt"
"tyapi-server/internal/domains/article/entities"
"tyapi-server/internal/domains/article/repositories"
"tyapi-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 (id, article_id, tag_id, created_at)
VALUES (UUID(), ?, ?, NOW())
`, 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
}

View File

@@ -145,6 +145,18 @@ func (h *ApiHandler) AddWhiteListIP(c *gin.Context) {
}
// DeleteWhiteListIP 删除白名单IP
// @Summary 删除白名单IP
// @Description 从当前用户的白名单中删除指定IP地址
// @Tags API管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param ip path string true "IP地址"
// @Success 200 {object} map[string]interface{} "删除白名单IP成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/my/whitelist/{ip} [delete]
func (h *ApiHandler) DeleteWhiteListIP(c *gin.Context) {
userID := h.getCurrentUserID(c)
if userID == "" {

View File

@@ -0,0 +1,650 @@
//nolint:unused
package handlers
import (
"tyapi-server/internal/application/article"
"tyapi-server/internal/application/article/dto/commands"
appQueries "tyapi-server/internal/application/article/dto/queries"
_ "tyapi-server/internal/application/article/dto/responses"
"tyapi-server/internal/shared/interfaces"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// ArticleHandler 文章HTTP处理器
type ArticleHandler struct {
appService article.ArticleApplicationService
responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
}
// NewArticleHandler 创建文章HTTP处理器
func NewArticleHandler(
appService article.ArticleApplicationService,
responseBuilder interfaces.ResponseBuilder,
validator interfaces.RequestValidator,
logger *zap.Logger,
) *ArticleHandler {
return &ArticleHandler{
appService: appService,
responseBuilder: responseBuilder,
validator: validator,
logger: logger,
}
}
// CreateArticle 创建文章
// @Summary 创建文章
// @Description 创建新的文章
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body commands.CreateArticleCommand true "创建文章请求"
// @Success 201 {object} map[string]interface{} "文章创建成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles [post]
func (h *ArticleHandler) CreateArticle(c *gin.Context) {
var cmd commands.CreateArticleCommand
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
// 验证用户是否已登录
if _, exists := c.Get("user_id"); !exists {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
if err := h.appService.CreateArticle(c.Request.Context(), &cmd); err != nil {
h.logger.Error("创建文章失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Created(c, nil, "文章创建成功")
}
// GetArticleByID 获取文章详情
// @Summary 获取文章详情
// @Description 根据ID获取文章详情
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "文章ID"
// @Success 200 {object} responses.ArticleInfoResponse "获取文章详情成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "文章不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles/{id} [get]
func (h *ArticleHandler) GetArticleByID(c *gin.Context) {
var query appQueries.GetArticleQuery
query.ID = c.Param("id")
if query.ID == "" {
h.responseBuilder.BadRequest(c, "文章ID不能为空")
return
}
response, err := h.appService.GetArticleByID(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取文章详情失败", zap.Error(err))
h.responseBuilder.NotFound(c, "文章不存在")
return
}
h.responseBuilder.Success(c, response, "获取文章详情成功")
}
// ListArticles 获取文章列表
// @Summary 获取文章列表
// @Description 分页获取文章列表,支持多种筛选条件
// @Tags 文章管理
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param status query string false "文章状态"
// @Param category_id query string false "分类ID"
// @Param tag_id query string false "标签ID"
// @Param title query string false "标题关键词"
// @Param summary query string false "摘要关键词"
// @Param is_featured query bool false "是否推荐"
// @Param order_by query string false "排序字段"
// @Param order_dir query string false "排序方向"
// @Success 200 {object} responses.ArticleListResponse "获取文章列表成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles [get]
func (h *ArticleHandler) ListArticles(c *gin.Context) {
var query appQueries.ListArticleQuery
if err := h.validator.ValidateQuery(c, &query); err != nil {
return
}
// 设置默认值
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 {
query.PageSize = 10
}
if query.PageSize > 100 {
query.PageSize = 100
}
response, err := h.appService.ListArticles(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取文章列表失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取文章列表失败")
return
}
h.responseBuilder.Success(c, response, "获取文章列表成功")
}
// UpdateArticle 更新文章
// @Summary 更新文章
// @Description 更新文章信息
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "文章ID"
// @Param request body commands.UpdateArticleCommand true "更新文章请求"
// @Success 200 {object} map[string]interface{} "文章更新成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "文章不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles/{id} [put]
func (h *ArticleHandler) UpdateArticle(c *gin.Context) {
var cmd commands.UpdateArticleCommand
cmd.ID = c.Param("id")
if cmd.ID == "" {
h.responseBuilder.BadRequest(c, "文章ID不能为空")
return
}
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.UpdateArticle(c.Request.Context(), &cmd); err != nil {
h.logger.Error("更新文章失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "文章更新成功")
}
// DeleteArticle 删除文章
// @Summary 删除文章
// @Description 删除指定文章
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "文章ID"
// @Success 200 {object} map[string]interface{} "文章删除成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "文章不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles/{id} [delete]
func (h *ArticleHandler) DeleteArticle(c *gin.Context) {
var cmd commands.DeleteArticleCommand
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.appService.DeleteArticle(c.Request.Context(), &cmd); err != nil {
h.logger.Error("删除文章失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "文章删除成功")
}
// PublishArticle 发布文章
// @Summary 发布文章
// @Description 将草稿文章发布
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "文章ID"
// @Success 200 {object} map[string]interface{} "文章发布成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "文章不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles/{id}/publish [post]
func (h *ArticleHandler) PublishArticle(c *gin.Context) {
var cmd commands.PublishArticleCommand
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.appService.PublishArticle(c.Request.Context(), &cmd); err != nil {
h.logger.Error("发布文章失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "文章发布成功")
}
// SchedulePublishArticle 定时发布文章
// @Summary 定时发布文章
// @Description 设置文章的定时发布时间
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "文章ID"
// @Param request body commands.SchedulePublishCommand true "定时发布请求"
// @Success 200 {object} map[string]interface{} "定时发布设置成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "文章不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/articles/{id}/schedule-publish [post]
func (h *ArticleHandler) SchedulePublishArticle(c *gin.Context) {
var cmd commands.SchedulePublishCommand
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.SchedulePublishArticle(c.Request.Context(), &cmd); err != nil {
h.logger.Error("设置定时发布失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "定时发布设置成功")
}
// ArchiveArticle 归档文章
// @Summary 归档文章
// @Description 将已发布文章归档
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "文章ID"
// @Success 200 {object} map[string]interface{} "文章归档成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "文章不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles/{id}/archive [post]
func (h *ArticleHandler) ArchiveArticle(c *gin.Context) {
var cmd commands.ArchiveArticleCommand
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.appService.ArchiveArticle(c.Request.Context(), &cmd); err != nil {
h.logger.Error("归档文章失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "文章归档成功")
}
// SetFeatured 设置推荐状态
// @Summary 设置推荐状态
// @Description 设置文章的推荐状态
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "文章ID"
// @Param request body commands.SetFeaturedCommand true "设置推荐状态请求"
// @Success 200 {object} map[string]interface{} "设置推荐状态成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "文章不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/articles/{id}/featured [put]
func (h *ArticleHandler) SetFeatured(c *gin.Context) {
var cmd commands.SetFeaturedCommand
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.SetFeatured(c.Request.Context(), &cmd); err != nil {
h.logger.Error("设置推荐状态失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "设置推荐状态成功")
}
// GetArticleStats 获取文章统计
// @Summary 获取文章统计
// @Description 获取文章相关统计数据
// @Tags 文章管理
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} responses.ArticleStatsResponse "获取统计成功"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/articles/stats [get]
func (h *ArticleHandler) GetArticleStats(c *gin.Context) {
response, err := h.appService.GetArticleStats(c.Request.Context())
if err != nil {
h.logger.Error("获取文章统计失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取文章统计失败")
return
}
h.responseBuilder.Success(c, response, "获取统计成功")
}
// ==================== 分类相关方法 ====================
// ListCategories 获取分类列表
// @Summary 获取分类列表
// @Description 获取所有文章分类
// @Tags 文章分类
// @Accept json
// @Produce json
// @Success 200 {object} responses.CategoryListResponse "获取分类列表成功"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/article-categories [get]
func (h *ArticleHandler) ListCategories(c *gin.Context) {
response, err := h.appService.ListCategories(c.Request.Context())
if err != nil {
h.logger.Error("获取分类列表失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取分类列表失败")
return
}
h.responseBuilder.Success(c, response, "获取分类列表成功")
}
// GetCategoryByID 获取分类详情
// @Summary 获取分类详情
// @Description 根据ID获取分类详情
// @Tags 文章分类
// @Accept json
// @Produce json
// @Param id path string true "分类ID"
// @Success 200 {object} responses.CategoryInfoResponse "获取分类详情成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 404 {object} map[string]interface{} "分类不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/article-categories/{id} [get]
func (h *ArticleHandler) GetCategoryByID(c *gin.Context) {
var query appQueries.GetCategoryQuery
query.ID = c.Param("id")
if query.ID == "" {
h.responseBuilder.BadRequest(c, "分类ID不能为空")
return
}
response, err := h.appService.GetCategoryByID(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取分类详情失败", zap.Error(err))
h.responseBuilder.NotFound(c, "分类不存在")
return
}
h.responseBuilder.Success(c, response, "获取分类详情成功")
}
// CreateCategory 创建分类
// @Summary 创建分类
// @Description 创建新的文章分类
// @Tags 文章分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body commands.CreateCategoryCommand true "创建分类请求"
// @Success 201 {object} map[string]interface{} "分类创建成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/article-categories [post]
func (h *ArticleHandler) CreateCategory(c *gin.Context) {
var cmd commands.CreateCategoryCommand
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.CreateCategory(c.Request.Context(), &cmd); err != nil {
h.logger.Error("创建分类失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Created(c, nil, "分类创建成功")
}
// UpdateCategory 更新分类
// @Summary 更新分类
// @Description 更新分类信息
// @Tags 文章分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "分类ID"
// @Param request body commands.UpdateCategoryCommand true "更新分类请求"
// @Success 200 {object} map[string]interface{} "分类更新成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "分类不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/article-categories/{id} [put]
func (h *ArticleHandler) UpdateCategory(c *gin.Context) {
var cmd commands.UpdateCategoryCommand
cmd.ID = c.Param("id")
if cmd.ID == "" {
h.responseBuilder.BadRequest(c, "分类ID不能为空")
return
}
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.UpdateCategory(c.Request.Context(), &cmd); err != nil {
h.logger.Error("更新分类失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "分类更新成功")
}
// DeleteCategory 删除分类
// @Summary 删除分类
// @Description 删除指定分类
// @Tags 文章分类管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "分类ID"
// @Success 200 {object} map[string]interface{} "分类删除成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "分类不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/article-categories/{id} [delete]
func (h *ArticleHandler) DeleteCategory(c *gin.Context) {
var cmd commands.DeleteCategoryCommand
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.appService.DeleteCategory(c.Request.Context(), &cmd); err != nil {
h.logger.Error("删除分类失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "分类删除成功")
}
// ==================== 标签相关方法 ====================
// ListTags 获取标签列表
// @Summary 获取标签列表
// @Description 获取所有文章标签
// @Tags 文章标签
// @Accept json
// @Produce json
// @Success 200 {object} responses.TagListResponse "获取标签列表成功"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/article-tags [get]
func (h *ArticleHandler) ListTags(c *gin.Context) {
response, err := h.appService.ListTags(c.Request.Context())
if err != nil {
h.logger.Error("获取标签列表失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取标签列表失败")
return
}
h.responseBuilder.Success(c, response, "获取标签列表成功")
}
// GetTagByID 获取标签详情
// @Summary 获取标签详情
// @Description 根据ID获取标签详情
// @Tags 文章标签
// @Accept json
// @Produce json
// @Param id path string true "标签ID"
// @Success 200 {object} responses.TagInfoResponse "获取标签详情成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 404 {object} map[string]interface{} "标签不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/article-tags/{id} [get]
func (h *ArticleHandler) GetTagByID(c *gin.Context) {
var query appQueries.GetTagQuery
query.ID = c.Param("id")
if query.ID == "" {
h.responseBuilder.BadRequest(c, "标签ID不能为空")
return
}
response, err := h.appService.GetTagByID(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取标签详情失败", zap.Error(err))
h.responseBuilder.NotFound(c, "标签不存在")
return
}
h.responseBuilder.Success(c, response, "获取标签详情成功")
}
// CreateTag 创建标签
// @Summary 创建标签
// @Description 创建新的文章标签
// @Tags 文章标签管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body commands.CreateTagCommand true "创建标签请求"
// @Success 201 {object} map[string]interface{} "标签创建成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/article-tags [post]
func (h *ArticleHandler) CreateTag(c *gin.Context) {
var cmd commands.CreateTagCommand
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.CreateTag(c.Request.Context(), &cmd); err != nil {
h.logger.Error("创建标签失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Created(c, nil, "标签创建成功")
}
// UpdateTag 更新标签
// @Summary 更新标签
// @Description 更新标签信息
// @Tags 文章标签管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "标签ID"
// @Param request body commands.UpdateTagCommand true "更新标签请求"
// @Success 200 {object} map[string]interface{} "标签更新成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "标签不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/article-tags/{id} [put]
func (h *ArticleHandler) UpdateTag(c *gin.Context) {
var cmd commands.UpdateTagCommand
cmd.ID = c.Param("id")
if cmd.ID == "" {
h.responseBuilder.BadRequest(c, "标签ID不能为空")
return
}
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
return
}
if err := h.appService.UpdateTag(c.Request.Context(), &cmd); err != nil {
h.logger.Error("更新标签失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "标签更新成功")
}
// DeleteTag 删除标签
// @Summary 删除标签
// @Description 删除指定标签
// @Tags 文章标签管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "标签ID"
// @Success 200 {object} map[string]interface{} "标签删除成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "标签不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/article-tags/{id} [delete]
func (h *ArticleHandler) DeleteTag(c *gin.Context) {
var cmd commands.DeleteTagCommand
if err := h.validator.ValidateParam(c, &cmd); err != nil {
return
}
if err := h.appService.DeleteTag(c.Request.Context(), &cmd); err != nil {
h.logger.Error("删除标签失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "标签删除成功")
}

View File

@@ -1,3 +1,4 @@
//nolint:unused
package handlers
import (
@@ -12,6 +13,7 @@ import (
"tyapi-server/internal/application/certification"
"tyapi-server/internal/application/certification/dto/commands"
"tyapi-server/internal/application/certification/dto/queries"
_ "tyapi-server/internal/application/certification/dto/responses"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/middleware"
)
@@ -123,8 +125,8 @@ func (h *CertificationHandler) SubmitEnterpriseInfo(c *gin.Context) {
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body commands.ConfirmAuthCommand true "确认状态请求"
// @Success 200 {object} responses.ConfirmStatusResponse "状态确认成功"
// @Param request body queries.ConfirmAuthCommand true "确认状态请求"
// @Success 200 {object} responses.ConfirmAuthResponse "状态确认成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "认证记录不存在"
@@ -155,8 +157,8 @@ func (h *CertificationHandler) ConfirmAuth(c *gin.Context) {
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body commands.ConfirmSignCommand true "确认状态请求"
// @Success 200 {object} responses.ConfirmStatusResponse "状态确认成功"
// @Param request body queries.ConfirmSignCommand true "确认状态请求"
// @Success 200 {object} responses.ConfirmSignResponse "状态确认成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "认证记录不存在"
@@ -260,15 +262,13 @@ func (h *CertificationHandler) ListCertifications(c *gin.Context) {
// HandleEsignCallback 处理e签宝回调
// @Summary 处理e签宝回调
// @Description 处理e签宝的企业认证和合同签署回调
// @Description 处理e签宝的异步回调通知
// @Tags 认证管理
// @Accept json
// @Produce json
// @Param request body commands.EsignCallbackCommand true "e签宝回调数据"
// @Success 200 {object} responses.CallbackResponse "回调处理成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/certifications/callbacks/esign [post]
// @Accept application/json
// @Produce text/plain
// @Success 200 {string} string "success"
// @Failure 400 {string} string "fail"
// @Router /api/v1/certifications/esign/callback [post]
func (h *CertificationHandler) HandleEsignCallback(c *gin.Context) {
// 记录请求基本信息
h.logger.Info("收到e签宝回调请求",

View File

@@ -1,3 +1,4 @@
//nolint:unused
package handlers
import (
@@ -12,6 +13,7 @@ import (
"tyapi-server/internal/application/finance"
"tyapi-server/internal/application/finance/dto/commands"
"tyapi-server/internal/application/finance/dto/queries"
_ "tyapi-server/internal/application/finance/dto/responses"
"tyapi-server/internal/shared/interfaces"
)
@@ -570,9 +572,9 @@ func (h *FinanceHandler) GetAlipayOrderStatus(c *gin.Context) {
// @Accept json
// @Produce json
// @Param request body finance.ApplyInvoiceRequest true "申请开票请求"
// @Success 200 {object} response.Response{data=finance.InvoiceApplicationResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse{data=dto.InvoiceApplicationResponse}
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/invoices/apply [post]
func (h *FinanceHandler) ApplyInvoice(c *gin.Context) {
var req finance.ApplyInvoiceRequest
@@ -601,9 +603,9 @@ func (h *FinanceHandler) ApplyInvoice(c *gin.Context) {
// @Description 获取用户的发票信息
// @Tags 发票管理
// @Produce json
// @Success 200 {object} response.Response{data=finance.InvoiceInfoResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse{data=dto.InvoiceInfoResponse}
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/invoices/info [get]
func (h *FinanceHandler) GetUserInvoiceInfo(c *gin.Context) {
userID := c.GetString("user_id")
@@ -628,9 +630,9 @@ func (h *FinanceHandler) GetUserInvoiceInfo(c *gin.Context) {
// @Accept json
// @Produce json
// @Param request body finance.UpdateInvoiceInfoRequest true "更新发票信息请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/invoices/info [put]
func (h *FinanceHandler) UpdateUserInvoiceInfo(c *gin.Context) {
var req finance.UpdateInvoiceInfoRequest
@@ -662,9 +664,9 @@ func (h *FinanceHandler) UpdateUserInvoiceInfo(c *gin.Context) {
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param status query string false "状态筛选"
// @Success 200 {object} response.Response{data=finance.InvoiceRecordsResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse{data=dto.InvoiceRecordsResponse}
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/invoices/records [get]
func (h *FinanceHandler) GetUserInvoiceRecords(c *gin.Context) {
userID := c.GetString("user_id")
@@ -703,8 +705,8 @@ func (h *FinanceHandler) GetUserInvoiceRecords(c *gin.Context) {
// @Produce application/octet-stream
// @Param application_id path string true "申请ID"
// @Success 200 {file} file
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/invoices/{application_id}/download [get]
func (h *FinanceHandler) DownloadInvoiceFile(c *gin.Context) {
userID := c.GetString("user_id")
@@ -739,9 +741,9 @@ func (h *FinanceHandler) DownloadInvoiceFile(c *gin.Context) {
// @Description 获取用户当前可开票的金额
// @Tags 发票管理
// @Produce json
// @Success 200 {object} response.Response{data=finance.AvailableAmountResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse{data=dto.AvailableAmountResponse}
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/invoices/available-amount [get]
func (h *FinanceHandler) GetAvailableAmount(c *gin.Context) {
userID := c.GetString("user_id")
@@ -771,9 +773,9 @@ func (h *FinanceHandler) GetAvailableAmount(c *gin.Context) {
// @Param status query string false "状态筛选pending/completed/rejected"
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
// @Success 200 {object} response.Response{data=finance.PendingApplicationsResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse{data=dto.PendingApplicationsResponse}
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/admin/invoices/pending [get]
func (h *FinanceHandler) GetPendingApplications(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
@@ -808,9 +810,9 @@ func (h *FinanceHandler) GetPendingApplications(c *gin.Context) {
// @Param application_id path string true "申请ID"
// @Param file formData file true "发票文件"
// @Param admin_notes formData string false "管理员备注"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/admin/invoices/{application_id}/approve [post]
func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) {
applicationID := c.Param("application_id")
@@ -860,9 +862,9 @@ func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) {
// @Produce json
// @Param application_id path string true "申请ID"
// @Param request body finance.RejectInvoiceRequest true "拒绝申请请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Success 200 {object} interfaces.APIResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/admin/invoices/{application_id}/reject [post]
func (h *FinanceHandler) RejectInvoiceApplication(c *gin.Context) {
applicationID := c.Param("application_id")
@@ -893,8 +895,8 @@ func (h *FinanceHandler) RejectInvoiceApplication(c *gin.Context) {
// @Produce application/octet-stream
// @Param application_id path string true "申请ID"
// @Success 200 {file} file
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/admin/invoices/{application_id}/download [get]
func (h *FinanceHandler) AdminDownloadInvoiceFile(c *gin.Context) {
applicationID := c.Param("application_id")
@@ -918,13 +920,16 @@ func (h *FinanceHandler) AdminDownloadInvoiceFile(c *gin.Context) {
c.Data(http.StatusOK, "application/pdf", result.FileContent)
}
// DebugEventSystem 调试事件系统状态
// @Summary 调试事件系统状态
// @Description 获取事件系统的调试信息
// @Tags 调试
// DebugEventSystem 调试事件系统
// @Summary 调试事件系统
// @Description 调试事件系统,用于测试事件触发和处理
// @Tags 系统调试
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/debug/events [get]
// @Security Bearer
// @Success 200 {object} map[string]interface{} "调试成功"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/debug/event-system [post]
func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
h.logger.Info("🔍 请求事件系统调试信息")

View File

@@ -1113,85 +1113,7 @@ func (h *ProductAdminHandler) DeleteProductDocumentation(c *gin.Context) {
h.responseBuilder.Success(c, nil, "文档删除成功")
}
// GetAdminApiCalls 获取管理端API调用记录
// @Summary 获取管理端API调用记录
// @Description 管理员获取API调用记录支持筛选和分页
// @Tags API管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param user_id query string false "用户ID"
// @Param transaction_id query string false "交易ID"
// @Param product_name query string false "产品名称"
// @Param status query string false "状态"
// @Param start_time query string false "开始时间" format(date-time)
// @Param end_time query string false "结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} dto.ApiCallListResponse "获取API调用记录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/api-calls [get]
func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 用户ID筛选
if userId := c.Query("user_id"); userId != "" {
filters["user_id"] = userId
}
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
filters["start_time"] = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
filters["end_time"] = t
}
}
// 交易ID筛选
if transactionId := c.Query("transaction_id"); transactionId != "" {
filters["transaction_id"] = transactionId
}
// 产品名称筛选
if productName := c.Query("product_name"); productName != "" {
filters["product_name"] = productName
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 构建分页选项
options := interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
}
result, err := h.apiAppService.GetAdminApiCalls(c.Request.Context(), filters, options)
if err != nil {
h.logger.Error("获取管理端API调用记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取API调用记录失败")
return
}
h.responseBuilder.Success(c, result, "获取API调用记录成功")
}
// GetAdminWalletTransactions 获取管理端消费记录
// @Summary 获取管理端消费记录
@@ -1211,7 +1133,7 @@ func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
// @Param end_time query string false "结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} dto.WalletTransactionListResponse "获取消费记录成功"
// @Success 200 {object} responses.WalletTransactionListResponse "获取消费记录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
@@ -1295,7 +1217,7 @@ func (h *ProductAdminHandler) GetAdminWalletTransactions(c *gin.Context) {
// @Param end_time query string false "结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} dto.RechargeRecordListResponse "获取充值记录成功"
// @Success 200 {object} responses.RechargeRecordListResponse "获取充值记录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"

View File

@@ -1,3 +1,4 @@
//nolint:unused
package handlers
import (
@@ -5,6 +6,7 @@ import (
"tyapi-server/internal/application/product"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
_ "tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/shared/interfaces"
"github.com/gin-gonic/gin"
@@ -607,9 +609,9 @@ func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) {
// @Accept json
// @Produce json
// @Param id path string true "产品ID"
// @Success 200 {object} responses.DocumentationResponse "获取文档成功"
// @Success 200 {object} responses.DocumentationResponse "获取产品文档成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 404 {object} map[string]interface{} "产品或文档不存在"
// @Failure 404 {object} map[string]interface{} "产品不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/products/{id}/documentation [get]
func (h *ProductHandler) GetProductDocumentation(c *gin.Context) {

View File

@@ -1,3 +1,4 @@
//nolint:unused
package handlers
import (
@@ -9,6 +10,7 @@ import (
"tyapi-server/internal/application/user"
"tyapi-server/internal/application/user/dto/commands"
"tyapi-server/internal/application/user/dto/queries"
_ "tyapi-server/internal/application/user/dto/responses"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/middleware"
)
@@ -362,9 +364,9 @@ func (h *UserHandler) GetUserDetail(c *gin.Context) {
h.response.Success(c, resp, "获取用户详情成功")
}
// GetUserStats 管理员获取用户统计信息
// @Summary 管理员获取用户统计信息
// @Description 管理员获取用户统计信息,包括总用户数、活跃用户数、已认证用户数
// GetUserStats 获取用户统计信息
// @Summary 获取用户统计信息
// @Description 管理员获取用户相关的统计信息
// @Tags 用户管理
// @Accept json
// @Produce json

View File

@@ -0,0 +1,104 @@
package routes
import (
"tyapi-server/internal/infrastructure/http/handlers"
sharedhttp "tyapi-server/internal/shared/http"
"tyapi-server/internal/shared/middleware"
"go.uber.org/zap"
)
// ArticleRoutes 文章路由
type ArticleRoutes struct {
handler *handlers.ArticleHandler
auth *middleware.JWTAuthMiddleware
admin *middleware.AdminAuthMiddleware
logger *zap.Logger
}
// NewArticleRoutes 创建文章路由
func NewArticleRoutes(
handler *handlers.ArticleHandler,
auth *middleware.JWTAuthMiddleware,
admin *middleware.AdminAuthMiddleware,
logger *zap.Logger,
) *ArticleRoutes {
return &ArticleRoutes{
handler: handler,
auth: auth,
admin: admin,
logger: logger,
}
}
// Register 注册路由
func (r *ArticleRoutes) Register(router *sharedhttp.GinRouter) {
engine := router.GetEngine()
// ==================== 用户端路由 ====================
// 文章相关路由 - 用户端
articleGroup := engine.Group("/api/v1/articles")
{
// 公开路由 - 不需要认证
articleGroup.GET("/:id", r.handler.GetArticleByID) // 获取文章详情
articleGroup.GET("", r.handler.ListArticles) // 获取文章列表(支持筛选:标题、分类、摘要、标签、推荐状态)
}
// 分类相关路由 - 用户端
categoryGroup := engine.Group("/api/v1/article-categories")
{
// 公开路由 - 不需要认证
categoryGroup.GET("", r.handler.ListCategories) // 获取分类列表
categoryGroup.GET("/:id", r.handler.GetCategoryByID) // 获取分类详情
}
// 标签相关路由 - 用户端
tagGroup := engine.Group("/api/v1/article-tags")
{
// 公开路由 - 不需要认证
tagGroup.GET("", r.handler.ListTags) // 获取标签列表
tagGroup.GET("/:id", r.handler.GetTagByID) // 获取标签详情
}
// ==================== 管理员端路由 ====================
// 管理员文章管理路由
adminArticleGroup := engine.Group("/api/v1/admin/articles")
adminArticleGroup.Use(r.admin.Handle())
{
// 统计信息
adminArticleGroup.GET("/stats", r.handler.GetArticleStats) // 获取文章统计
// 文章管理
adminArticleGroup.POST("", r.handler.CreateArticle) // 创建文章
adminArticleGroup.PUT("/:id", r.handler.UpdateArticle) // 更新文章
adminArticleGroup.DELETE("/:id", r.handler.DeleteArticle) // 删除文章
// 文章状态管理
adminArticleGroup.POST("/:id/publish", r.handler.PublishArticle) // 发布文章
adminArticleGroup.POST("/:id/schedule-publish", r.handler.SchedulePublishArticle) // 定时发布文章
adminArticleGroup.POST("/:id/archive", r.handler.ArchiveArticle) // 归档文章
adminArticleGroup.PUT("/:id/featured", r.handler.SetFeatured) // 设置推荐状态
}
// 管理员分类管理路由
adminCategoryGroup := engine.Group("/api/v1/admin/article-categories")
adminCategoryGroup.Use(r.admin.Handle())
{
// 分类管理
adminCategoryGroup.POST("", r.handler.CreateCategory) // 创建分类
adminCategoryGroup.PUT("/:id", r.handler.UpdateCategory) // 更新分类
adminCategoryGroup.DELETE("/:id", r.handler.DeleteCategory) // 删除分类
}
// 管理员标签管理路由
adminTagGroup := engine.Group("/api/v1/admin/article-tags")
adminTagGroup.Use(r.admin.Handle())
{
// 标签管理
adminTagGroup.POST("", r.handler.CreateTag) // 创建标签
adminTagGroup.PUT("/:id", r.handler.UpdateTag) // 更新标签
adminTagGroup.DELETE("/:id", r.handler.DeleteTag) // 删除标签
}
r.logger.Info("文章路由注册完成")
}

View File

@@ -81,11 +81,7 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
subscriptions.POST("/batch-update-prices", r.handler.BatchUpdateSubscriptionPrices)
}
// API调用记录管理
apiCalls := adminGroup.Group("/api-calls")
{
apiCalls.GET("", r.handler.GetAdminApiCalls)
}
// 消费记录管理
walletTransactions := adminGroup.Group("/wallet-transactions")

View File

@@ -0,0 +1,58 @@
package task
import (
"context"
"encoding/json"
"fmt"
"github.com/hibiken/asynq"
"go.uber.org/zap"
)
// ArticlePublisher 文章发布接口
type ArticlePublisher interface {
PublishArticleByID(ctx context.Context, articleID string) error
}
// ArticleTaskHandler 文章任务处理器
type ArticleTaskHandler struct {
publisher ArticlePublisher
logger *zap.Logger
}
// NewArticleTaskHandler 创建文章任务处理器
func NewArticleTaskHandler(
publisher ArticlePublisher,
logger *zap.Logger,
) *ArticleTaskHandler {
return &ArticleTaskHandler{
publisher: publisher,
logger: logger,
}
}
// HandleArticlePublish 处理文章定时发布任务
func (h *ArticleTaskHandler) HandleArticlePublish(ctx context.Context, t *asynq.Task) error {
var payload map[string]interface{}
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
h.logger.Error("解析任务载荷失败", zap.Error(err))
return fmt.Errorf("解析任务载荷失败: %w", err)
}
articleID, ok := payload["article_id"].(string)
if !ok {
h.logger.Error("任务载荷中缺少文章ID")
return fmt.Errorf("任务载荷中缺少文章ID")
}
// 执行文章发布
if err := h.publisher.PublishArticleByID(ctx, articleID); err != nil {
h.logger.Error("定时发布文章失败",
zap.String("article_id", articleID),
zap.Error(err))
return fmt.Errorf("定时发布文章失败: %w", err)
}
h.logger.Info("定时发布文章成功", zap.String("article_id", articleID))
return nil
}

View File

@@ -0,0 +1,75 @@
package task
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/hibiken/asynq"
"go.uber.org/zap"
)
// AsynqClient Asynq 客户端
type AsynqClient struct {
client *asynq.Client
logger *zap.Logger
}
// NewAsynqClient 创建 Asynq 客户端
func NewAsynqClient(redisAddr string, logger *zap.Logger) *AsynqClient {
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
return &AsynqClient{
client: client,
logger: logger,
}
}
// Close 关闭客户端
func (c *AsynqClient) Close() error {
return c.client.Close()
}
// ScheduleArticlePublish 调度文章定时发布任务
func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID string, publishTime time.Time) error {
payload := map[string]interface{}{
"article_id": articleID,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
c.logger.Error("序列化任务载荷失败", zap.Error(err))
return fmt.Errorf("创建任务失败: %w", err)
}
task := asynq.NewTask(TaskTypeArticlePublish, payloadBytes)
// 计算延迟时间
delay := publishTime.Sub(time.Now())
if delay <= 0 {
return fmt.Errorf("定时发布时间不能早于当前时间")
}
// 设置任务选项
opts := []asynq.Option{
asynq.ProcessIn(delay),
asynq.MaxRetry(3),
asynq.Timeout(5 * time.Minute),
}
info, err := c.client.Enqueue(task, opts...)
if err != nil {
c.logger.Error("调度定时发布任务失败",
zap.String("article_id", articleID),
zap.Time("publish_time", publishTime),
zap.Error(err))
return fmt.Errorf("调度任务失败: %w", err)
}
c.logger.Info("定时发布任务调度成功",
zap.String("article_id", articleID),
zap.Time("publish_time", publishTime),
zap.String("task_id", info.ID))
return nil
}

View File

@@ -0,0 +1,98 @@
package task
import (
"fmt"
"github.com/hibiken/asynq"
"go.uber.org/zap"
)
// AsynqWorker Asynq Worker
type AsynqWorker struct {
server *asynq.Server
mux *asynq.ServeMux
taskHandler *ArticleTaskHandler
logger *zap.Logger
}
// NewAsynqWorker 创建 Asynq Worker
func NewAsynqWorker(
redisAddr string,
taskHandler *ArticleTaskHandler,
logger *zap.Logger,
) *AsynqWorker {
server := asynq.NewServer(
asynq.RedisClientOpt{Addr: redisAddr},
asynq.Config{
Concurrency: 10, // 并发数
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
Logger: NewAsynqLogger(logger),
},
)
mux := asynq.NewServeMux()
return &AsynqWorker{
server: server,
mux: mux,
taskHandler: taskHandler,
logger: logger,
}
}
// RegisterHandlers 注册任务处理器
func (w *AsynqWorker) RegisterHandlers() {
// 注册文章定时发布任务处理器
w.mux.HandleFunc(TaskTypeArticlePublish, w.taskHandler.HandleArticlePublish)
w.logger.Info("任务处理器注册完成")
}
// Start 启动 Worker
func (w *AsynqWorker) Start() error {
w.RegisterHandlers()
w.logger.Info("启动 Asynq Worker")
return w.server.Run(w.mux)
}
// Stop 停止 Worker
func (w *AsynqWorker) Stop() {
w.logger.Info("停止 Asynq Worker")
w.server.Stop()
w.server.Shutdown()
}
// AsynqLogger Asynq 日志适配器
type AsynqLogger struct {
logger *zap.Logger
}
// NewAsynqLogger 创建 Asynq 日志适配器
func NewAsynqLogger(logger *zap.Logger) *AsynqLogger {
return &AsynqLogger{logger: logger}
}
func (l *AsynqLogger) Debug(args ...interface{}) {
l.logger.Debug(fmt.Sprint(args...))
}
func (l *AsynqLogger) Info(args ...interface{}) {
l.logger.Info(fmt.Sprint(args...))
}
func (l *AsynqLogger) Warn(args ...interface{}) {
l.logger.Warn(fmt.Sprint(args...))
}
func (l *AsynqLogger) Error(args ...interface{}) {
l.logger.Error(fmt.Sprint(args...))
}
func (l *AsynqLogger) Fatal(args ...interface{}) {
l.logger.Fatal(fmt.Sprint(args...))
}

View File

@@ -0,0 +1,7 @@
package task
// 任务类型常量
const (
// TaskTypeArticlePublish 文章定时发布任务
TaskTypeArticlePublish = "article:publish"
)