add article
This commit is contained in:
@@ -502,18 +502,18 @@ func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filter
|
||||
if call.ProductId != nil && *call.ProductId != "" {
|
||||
item.ProductId = call.ProductId
|
||||
}
|
||||
|
||||
|
||||
// 从映射中获取产品名称
|
||||
if productName, exists := productNameMap[call.ID]; exists && productName != "" {
|
||||
item.ProductName = &productName
|
||||
}
|
||||
|
||||
|
||||
// 安全设置结束时间
|
||||
if call.EndAt != nil && !call.EndAt.IsZero() {
|
||||
endAt := call.EndAt.Format("2006-01-02 15:04:05")
|
||||
item.EndAt = &endAt
|
||||
}
|
||||
|
||||
|
||||
// 安全设置费用
|
||||
if call.Cost != nil {
|
||||
cost := call.Cost.String()
|
||||
@@ -521,12 +521,12 @@ func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filter
|
||||
item.Cost = &cost
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 安全设置错误类型
|
||||
if call.ErrorType != nil && *call.ErrorType != "" {
|
||||
item.ErrorType = call.ErrorType
|
||||
}
|
||||
|
||||
|
||||
// 安全设置错误信息
|
||||
if call.ErrorMsg != nil && *call.ErrorMsg != "" {
|
||||
item.ErrorMsg = call.ErrorMsg
|
||||
@@ -541,24 +541,24 @@ func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filter
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, *call.UserId)
|
||||
if err == nil && user.ID != "" {
|
||||
companyName := "未知企业"
|
||||
|
||||
|
||||
// 安全获取企业名称
|
||||
if user.EnterpriseInfo != nil && user.EnterpriseInfo.CompanyName != "" {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
|
||||
item.CompanyName = &companyName
|
||||
|
||||
|
||||
// 安全构建用户响应
|
||||
item.User = &dto.UserSimpleResponse{
|
||||
ID: user.ID,
|
||||
CompanyName: companyName,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
|
||||
|
||||
// 验证用户数据的完整性
|
||||
if user.Phone == "" {
|
||||
s.logger.Warn("用户手机号为空",
|
||||
s.logger.Warn("用户手机号为空",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("call_id", call.ID))
|
||||
item.User.Phone = "未知手机号"
|
||||
@@ -566,16 +566,16 @@ func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filter
|
||||
} else {
|
||||
// 用户查询失败或用户数据不完整时的处理
|
||||
if err != nil {
|
||||
s.logger.Warn("获取用户信息失败",
|
||||
s.logger.Warn("获取用户信息失败",
|
||||
zap.String("user_id", *call.UserId),
|
||||
zap.String("call_id", call.ID),
|
||||
zap.Error(err))
|
||||
} else if user.ID == "" {
|
||||
s.logger.Warn("用户ID为空",
|
||||
s.logger.Warn("用户ID为空",
|
||||
zap.String("call_user_id", *call.UserId),
|
||||
zap.String("call_id", call.ID))
|
||||
}
|
||||
|
||||
|
||||
// 设置默认值
|
||||
defaultCompanyName := "未知企业"
|
||||
item.CompanyName = &defaultCompanyName
|
||||
|
||||
45
internal/application/article/article_application_service.go
Normal file
45
internal/application/article/article_application_service.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/application/article/dto/commands"
|
||||
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||
"tyapi-server/internal/application/article/dto/responses"
|
||||
)
|
||||
|
||||
// ArticleApplicationService 文章应用服务接口
|
||||
type ArticleApplicationService interface {
|
||||
// 文章管理
|
||||
CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error
|
||||
UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error
|
||||
DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error
|
||||
GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error)
|
||||
ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
|
||||
|
||||
// 文章状态管理
|
||||
PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error
|
||||
PublishArticleByID(ctx context.Context, articleID string) error
|
||||
SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
|
||||
ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error
|
||||
SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error
|
||||
|
||||
// 文章交互
|
||||
RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error
|
||||
|
||||
// 统计信息
|
||||
GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error)
|
||||
|
||||
// 分类管理
|
||||
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
|
||||
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
|
||||
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
|
||||
GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||
ListCategories(ctx context.Context) (*responses.CategoryListResponse, error)
|
||||
|
||||
// 标签管理
|
||||
CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error
|
||||
UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error
|
||||
DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error
|
||||
GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error)
|
||||
ListTags(ctx context.Context) (*responses.TagListResponse, error)
|
||||
}
|
||||
690
internal/application/article/article_application_service_impl.go
Normal file
690
internal/application/article/article_application_service_impl.go
Normal file
@@ -0,0 +1,690 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
|
||||
"tyapi-server/internal/domains/article/services"
|
||||
"tyapi-server/internal/infrastructure/task"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ArticleApplicationServiceImpl 文章应用服务实现
|
||||
type ArticleApplicationServiceImpl struct {
|
||||
articleRepo repositories.ArticleRepository
|
||||
categoryRepo repositories.CategoryRepository
|
||||
tagRepo repositories.TagRepository
|
||||
articleService *services.ArticleService
|
||||
asynqClient *task.AsynqClient
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewArticleApplicationService 创建文章应用服务
|
||||
func NewArticleApplicationService(
|
||||
articleRepo repositories.ArticleRepository,
|
||||
categoryRepo repositories.CategoryRepository,
|
||||
tagRepo repositories.TagRepository,
|
||||
articleService *services.ArticleService,
|
||||
asynqClient *task.AsynqClient,
|
||||
logger *zap.Logger,
|
||||
) ArticleApplicationService {
|
||||
return &ArticleApplicationServiceImpl{
|
||||
articleRepo: articleRepo,
|
||||
categoryRepo: categoryRepo,
|
||||
tagRepo: tagRepo,
|
||||
articleService: articleService,
|
||||
asynqClient: asynqClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateArticle 创建文章
|
||||
func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateArticle(cmd); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建文章实体
|
||||
article := &entities.Article{
|
||||
Title: cmd.Title,
|
||||
Content: cmd.Content,
|
||||
Summary: cmd.Summary,
|
||||
CoverImage: cmd.CoverImage,
|
||||
CategoryID: cmd.CategoryID,
|
||||
IsFeatured: cmd.IsFeatured,
|
||||
Status: entities.ArticleStatusDraft,
|
||||
}
|
||||
|
||||
// 3. 调用领域服务验证
|
||||
if err := s.articleService.ValidateArticle(article); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 保存文章
|
||||
_, err := s.articleRepo.Create(ctx, *article)
|
||||
if err != nil {
|
||||
s.logger.Error("创建文章失败", zap.Error(err))
|
||||
return fmt.Errorf("创建文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 处理标签关联
|
||||
if len(cmd.TagIDs) > 0 {
|
||||
for _, tagID := range cmd.TagIDs {
|
||||
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
|
||||
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("创建文章成功", zap.String("id", article.ID), zap.String("title", article.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateArticle 更新文章
|
||||
func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error {
|
||||
// 1. 获取原文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以编辑
|
||||
if !article.CanEdit() {
|
||||
return fmt.Errorf("文章状态不允许编辑")
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
if cmd.Title != "" {
|
||||
article.Title = cmd.Title
|
||||
}
|
||||
if cmd.Content != "" {
|
||||
article.Content = cmd.Content
|
||||
}
|
||||
if cmd.Summary != "" {
|
||||
article.Summary = cmd.Summary
|
||||
}
|
||||
if cmd.CoverImage != "" {
|
||||
article.CoverImage = cmd.CoverImage
|
||||
}
|
||||
if cmd.CategoryID != "" {
|
||||
article.CategoryID = cmd.CategoryID
|
||||
}
|
||||
article.IsFeatured = cmd.IsFeatured
|
||||
|
||||
// 4. 验证更新后的文章
|
||||
if err := s.articleService.ValidateArticle(&article); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 6. 处理标签关联
|
||||
if len(cmd.TagIDs) > 0 {
|
||||
// 先清除现有标签
|
||||
existingTags, _ := s.tagRepo.GetArticleTags(ctx, article.ID)
|
||||
for _, tag := range existingTags {
|
||||
s.tagRepo.RemoveTagFromArticle(ctx, article.ID, tag.ID)
|
||||
}
|
||||
|
||||
// 添加新标签
|
||||
for _, tagID := range cmd.TagIDs {
|
||||
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
|
||||
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("更新文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteArticle 删除文章
|
||||
func (s *ArticleApplicationServiceImpl) DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error {
|
||||
// 1. 检查文章是否存在
|
||||
_, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 删除文章
|
||||
if err := s.articleRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除文章成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetArticleByID 根据ID获取文章
|
||||
func (s *ArticleApplicationServiceImpl) GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error) {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := responses.FromArticleEntity(&article)
|
||||
|
||||
s.logger.Info("获取文章成功", zap.String("id", article.ID))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListArticles 获取文章列表
|
||||
func (s *ArticleApplicationServiceImpl) ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
|
||||
// 1. 构建仓储查询
|
||||
repoQuery := &repoQueries.ListArticleQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Status: query.Status,
|
||||
CategoryID: query.CategoryID,
|
||||
TagID: query.TagID,
|
||||
Title: query.Title,
|
||||
Summary: query.Summary,
|
||||
IsFeatured: query.IsFeatured,
|
||||
OrderBy: query.OrderBy,
|
||||
OrderDir: query.OrderDir,
|
||||
}
|
||||
|
||||
// 2. 调用仓储
|
||||
articles, total, err := s.articleRepo.ListArticles(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取文章列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 转换为响应对象
|
||||
items := responses.FromArticleEntities(articles)
|
||||
|
||||
response := &responses.ArticleListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}
|
||||
|
||||
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
||||
// PublishArticle 发布文章
|
||||
func (s *ArticleApplicationServiceImpl) PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 发布文章
|
||||
if err := article.Publish(); err != nil {
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("发布文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishArticleByID 通过ID发布文章 (用于定时任务)
|
||||
func (s *ArticleApplicationServiceImpl) PublishArticleByID(ctx context.Context, articleID string) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, articleID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", articleID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 发布文章
|
||||
if err := article.Publish(); err != nil {
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("定时发布文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchedulePublishArticle 定时发布文章
|
||||
func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 设置定时发布
|
||||
if err := article.SchedulePublish(cmd.ScheduledTime); err != nil {
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 调度定时发布任务
|
||||
if err := s.asynqClient.ScheduleArticlePublish(ctx, cmd.ID, cmd.ScheduledTime); err != nil {
|
||||
s.logger.Error("调度定时发布任务失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("调度定时发布任务失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", cmd.ScheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveArticle 归档文章
|
||||
func (s *ArticleApplicationServiceImpl) ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 归档文章
|
||||
if err := article.Archive(); err != nil {
|
||||
return fmt.Errorf("归档文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("归档文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("归档文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFeatured 设置推荐状态
|
||||
func (s *ArticleApplicationServiceImpl) SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 设置推荐状态
|
||||
article.SetFeatured(cmd.IsFeatured)
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("设置推荐状态失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("设置推荐状态成功", zap.String("id", article.ID), zap.Bool("is_featured", cmd.IsFeatured))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
// RecordView 记录阅读
|
||||
func (s *ArticleApplicationServiceImpl) RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error {
|
||||
// 1. 增加阅读量
|
||||
if err := s.articleRepo.IncrementViewCount(ctx, articleID); err != nil {
|
||||
s.logger.Error("增加阅读量失败", zap.String("id", articleID), zap.Error(err))
|
||||
return fmt.Errorf("记录阅读失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("记录阅读成功", zap.String("id", articleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetArticleStats 获取文章统计
|
||||
func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error) {
|
||||
// 1. 获取各种统计
|
||||
totalArticles, err := s.articleRepo.CountByStatus(ctx, "")
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章总数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
publishedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusPublished)
|
||||
if err != nil {
|
||||
s.logger.Error("获取已发布文章数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
draftArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusDraft)
|
||||
if err != nil {
|
||||
s.logger.Error("获取草稿文章数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
archivedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusArchived)
|
||||
if err != nil {
|
||||
s.logger.Error("获取归档文章数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
response := &responses.ArticleStatsResponse{
|
||||
TotalArticles: totalArticles,
|
||||
PublishedArticles: publishedArticles,
|
||||
DraftArticles: draftArticles,
|
||||
ArchivedArticles: archivedArticles,
|
||||
TotalViews: 0, // TODO: 实现总阅读量统计
|
||||
}
|
||||
|
||||
s.logger.Info("获取文章统计成功")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
||||
// validateCreateArticle 验证创建文章参数
|
||||
func (s *ArticleApplicationServiceImpl) validateCreateArticle(cmd *commands.CreateArticleCommand) error {
|
||||
if cmd.Title == "" {
|
||||
return fmt.Errorf("文章标题不能为空")
|
||||
}
|
||||
if cmd.Content == "" {
|
||||
return fmt.Errorf("文章内容不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== 分类相关方法 ====================
|
||||
|
||||
// CreateCategory 创建分类
|
||||
func (s *ArticleApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateCategory(cmd); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建分类实体
|
||||
category := &entities.Category{
|
||||
Name: cmd.Name,
|
||||
Description: cmd.Description,
|
||||
}
|
||||
|
||||
// 3. 保存分类
|
||||
_, err := s.categoryRepo.Create(ctx, *category)
|
||||
if err != nil {
|
||||
s.logger.Error("创建分类失败", zap.Error(err))
|
||||
return fmt.Errorf("创建分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建分类成功", zap.String("id", category.ID), zap.String("name", category.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
func (s *ArticleApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
|
||||
// 1. 获取原分类
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
category.Name = cmd.Name
|
||||
category.Description = cmd.Description
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("更新分类失败", zap.String("id", category.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新分类成功", zap.String("id", category.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
func (s *ArticleApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
|
||||
// 1. 检查分类是否存在
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否有文章使用此分类
|
||||
count, err := s.articleRepo.CountByCategoryID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("检查分类使用情况失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除分类失败: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return fmt.Errorf("该分类下还有 %d 篇文章,无法删除", count)
|
||||
}
|
||||
|
||||
// 3. 删除分类
|
||||
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("name", category.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCategoryByID 获取分类详情
|
||||
func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
|
||||
// 1. 获取分类
|
||||
category, err := s.categoryRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := &responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
SortOrder: category.SortOrder,
|
||||
CreatedAt: category.CreatedAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListCategories 获取分类列表
|
||||
func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*responses.CategoryListResponse, error) {
|
||||
// 1. 获取分类列表
|
||||
categories, err := s.categoryRepo.List(ctx, interfaces.ListOptions{})
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
items := make([]responses.CategoryInfoResponse, len(categories))
|
||||
for i, category := range categories {
|
||||
items[i] = responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
SortOrder: category.SortOrder,
|
||||
CreatedAt: category.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response := &responses.CategoryListResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ==================== 标签相关方法 ====================
|
||||
|
||||
// CreateTag 创建标签
|
||||
func (s *ArticleApplicationServiceImpl) CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateTag(cmd); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建标签实体
|
||||
tag := &entities.Tag{
|
||||
Name: cmd.Name,
|
||||
Color: cmd.Color,
|
||||
}
|
||||
|
||||
// 3. 保存标签
|
||||
_, err := s.tagRepo.Create(ctx, *tag)
|
||||
if err != nil {
|
||||
s.logger.Error("创建标签失败", zap.Error(err))
|
||||
return fmt.Errorf("创建标签失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建标签成功", zap.String("id", tag.ID), zap.String("name", tag.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTag 更新标签
|
||||
func (s *ArticleApplicationServiceImpl) UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error {
|
||||
// 1. 获取原标签
|
||||
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("标签不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
tag.Name = cmd.Name
|
||||
tag.Color = cmd.Color
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.tagRepo.Update(ctx, tag); err != nil {
|
||||
s.logger.Error("更新标签失败", zap.String("id", tag.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新标签失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新标签成功", zap.String("id", tag.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTag 删除标签
|
||||
func (s *ArticleApplicationServiceImpl) DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error {
|
||||
// 1. 检查标签是否存在
|
||||
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("标签不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 删除标签
|
||||
if err := s.tagRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除标签失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除标签成功", zap.String("id", cmd.ID), zap.String("name", tag.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTagByID 获取标签详情
|
||||
func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error) {
|
||||
// 1. 获取标签
|
||||
tag, err := s.tagRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("标签不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := &responses.TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListTags 获取标签列表
|
||||
func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*responses.TagListResponse, error) {
|
||||
// 1. 获取标签列表
|
||||
tags, err := s.tagRepo.List(ctx, interfaces.ListOptions{})
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取标签列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
items := make([]responses.TagInfoResponse, len(tags))
|
||||
for i, tag := range tags {
|
||||
items[i] = responses.TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response := &responses.TagListResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ==================== 验证方法 ====================
|
||||
|
||||
// validateCreateCategory 验证创建分类参数
|
||||
func (s *ArticleApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return fmt.Errorf("分类名称不能为空")
|
||||
}
|
||||
if len(cmd.Name) > 50 {
|
||||
return fmt.Errorf("分类名称长度不能超过50个字符")
|
||||
}
|
||||
if len(cmd.Description) > 200 {
|
||||
return fmt.Errorf("分类描述长度不能超过200个字符")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCreateTag 验证创建标签参数
|
||||
func (s *ArticleApplicationServiceImpl) validateCreateTag(cmd *commands.CreateTagCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return fmt.Errorf("标签名称不能为空")
|
||||
}
|
||||
if len(cmd.Name) > 30 {
|
||||
return fmt.Errorf("标签名称长度不能超过30个字符")
|
||||
}
|
||||
if cmd.Color == "" {
|
||||
return fmt.Errorf("标签颜色不能为空")
|
||||
}
|
||||
// TODO: 添加十六进制颜色格式验证
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package commands
|
||||
|
||||
// CreateArticleCommand 创建文章命令
|
||||
type CreateArticleCommand struct {
|
||||
Title string `json:"title" binding:"required" comment:"文章标题"`
|
||||
Content string `json:"content" binding:"required" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
// UpdateArticleCommand 更新文章命令
|
||||
type UpdateArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
Title string `json:"title" comment:"文章标题"`
|
||||
Content string `json:"content" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
// DeleteArticleCommand 删除文章命令
|
||||
type DeleteArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// PublishArticleCommand 发布文章命令
|
||||
type PublishArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// ArchiveArticleCommand 归档文章命令
|
||||
type ArchiveArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// SetFeaturedCommand 设置推荐状态命令
|
||||
type SetFeaturedCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
IsFeatured bool `json:"is_featured" binding:"required" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
// CreateCategoryCommand 创建分类命令
|
||||
type CreateCategoryCommand struct {
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||
}
|
||||
|
||||
// UpdateCategoryCommand 更新分类命令
|
||||
type UpdateCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||
}
|
||||
|
||||
// DeleteCategoryCommand 删除分类命令
|
||||
type DeleteCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package commands
|
||||
|
||||
import "time"
|
||||
|
||||
// SchedulePublishCommand 定时发布文章命令
|
||||
type SchedulePublishCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
ScheduledTime time.Time `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
19
internal/application/article/dto/commands/tag_commands.go
Normal file
19
internal/application/article/dto/commands/tag_commands.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
// CreateTagCommand 创建标签命令
|
||||
type CreateTagCommand struct {
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||
}
|
||||
|
||||
// UpdateTagCommand 更新标签命令
|
||||
type UpdateTagCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||
}
|
||||
|
||||
// DeleteTagCommand 删除标签命令
|
||||
type DeleteTagCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
}
|
||||
54
internal/application/article/dto/queries/article_queries.go
Normal file
54
internal/application/article/dto/queries/article_queries.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package queries
|
||||
|
||||
import "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
// ListArticleQuery 文章列表查询
|
||||
type ListArticleQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
|
||||
CategoryID string `form:"category_id" comment:"分类ID"`
|
||||
TagID string `form:"tag_id" comment:"标签ID"`
|
||||
Title string `form:"title" comment:"标题关键词"`
|
||||
Summary string `form:"summary" comment:"摘要关键词"`
|
||||
IsFeatured *bool `form:"is_featured" comment:"是否推荐"`
|
||||
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// SearchArticleQuery 文章搜索查询
|
||||
type SearchArticleQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
Keyword string `form:"keyword" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" comment:"分类ID"`
|
||||
AuthorID string `form:"author_id" comment:"作者ID"`
|
||||
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
|
||||
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetArticleQuery 获取文章详情查询
|
||||
type GetArticleQuery struct {
|
||||
ID string `uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// GetArticlesByAuthorQuery 获取作者文章查询
|
||||
type GetArticlesByAuthorQuery struct {
|
||||
AuthorID string `uri:"author_id" binding:"required" comment:"作者ID"`
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
|
||||
// GetArticlesByCategoryQuery 获取分类文章查询
|
||||
type GetArticlesByCategoryQuery struct {
|
||||
CategoryID string `uri:"category_id" binding:"required" comment:"分类ID"`
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
|
||||
// GetFeaturedArticlesQuery 获取推荐文章查询
|
||||
type GetFeaturedArticlesQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package queries
|
||||
|
||||
// GetCategoryQuery 获取分类详情查询
|
||||
type GetCategoryQuery struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
}
|
||||
6
internal/application/article/dto/queries/tag_queries.go
Normal file
6
internal/application/article/dto/queries/tag_queries.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package queries
|
||||
|
||||
// GetTagQuery 获取标签详情查询
|
||||
type GetTagQuery struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
}
|
||||
135
internal/application/article/dto/responses/article_responses.go
Normal file
135
internal/application/article/dto/responses/article_responses.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
)
|
||||
|
||||
// ArticleInfoResponse 文章详情响应
|
||||
type ArticleInfoResponse struct {
|
||||
ID string `json:"id" comment:"文章ID"`
|
||||
Title string `json:"title" comment:"文章标题"`
|
||||
Content string `json:"content" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Status string `json:"status" comment:"文章状态"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ViewCount int `json:"view_count" comment:"阅读量"`
|
||||
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ArticleListResponse 文章列表响应
|
||||
type ArticleListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ArticleInfoResponse `json:"items" comment:"文章列表"`
|
||||
}
|
||||
|
||||
// CategoryInfoResponse 分类信息响应
|
||||
type CategoryInfoResponse struct {
|
||||
ID string `json:"id" comment:"分类ID"`
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Description string `json:"description" comment:"分类描述"`
|
||||
SortOrder int `json:"sort_order" comment:"排序"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
}
|
||||
|
||||
// TagInfoResponse 标签信息响应
|
||||
type TagInfoResponse struct {
|
||||
ID string `json:"id" comment:"标签ID"`
|
||||
Name string `json:"name" comment:"标签名称"`
|
||||
Color string `json:"color" comment:"标签颜色"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
}
|
||||
|
||||
// CategoryListResponse 分类列表响应
|
||||
type CategoryListResponse struct {
|
||||
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
|
||||
Total int `json:"total" comment:"总数"`
|
||||
}
|
||||
|
||||
// TagListResponse 标签列表响应
|
||||
type TagListResponse struct {
|
||||
Items []TagInfoResponse `json:"items" comment:"标签列表"`
|
||||
Total int `json:"total" comment:"总数"`
|
||||
}
|
||||
|
||||
|
||||
// ArticleStatsResponse 文章统计响应
|
||||
type ArticleStatsResponse struct {
|
||||
TotalArticles int64 `json:"total_articles" comment:"文章总数"`
|
||||
PublishedArticles int64 `json:"published_articles" comment:"已发布文章数"`
|
||||
DraftArticles int64 `json:"draft_articles" comment:"草稿文章数"`
|
||||
ArchivedArticles int64 `json:"archived_articles" comment:"归档文章数"`
|
||||
TotalViews int64 `json:"total_views" comment:"总阅读量"`
|
||||
}
|
||||
|
||||
// FromArticleEntity 从文章实体转换为响应对象
|
||||
func FromArticleEntity(article *entities.Article) *ArticleInfoResponse {
|
||||
if article == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := &ArticleInfoResponse{
|
||||
ID: article.ID,
|
||||
Title: article.Title,
|
||||
Content: article.Content,
|
||||
Summary: article.Summary,
|
||||
CoverImage: article.CoverImage,
|
||||
CategoryID: article.CategoryID,
|
||||
Status: string(article.Status),
|
||||
IsFeatured: article.IsFeatured,
|
||||
PublishedAt: article.PublishedAt,
|
||||
ViewCount: article.ViewCount,
|
||||
CreatedAt: article.CreatedAt,
|
||||
UpdatedAt: article.UpdatedAt,
|
||||
}
|
||||
|
||||
// 转换分类信息
|
||||
if article.Category != nil {
|
||||
response.Category = &CategoryInfoResponse{
|
||||
ID: article.Category.ID,
|
||||
Name: article.Category.Name,
|
||||
Description: article.Category.Description,
|
||||
SortOrder: article.Category.SortOrder,
|
||||
CreatedAt: article.Category.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 转换标签信息
|
||||
if len(article.Tags) > 0 {
|
||||
response.Tags = make([]TagInfoResponse, len(article.Tags))
|
||||
for i, tag := range article.Tags {
|
||||
response.Tags[i] = TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// FromArticleEntities 从文章实体列表转换为响应对象列表
|
||||
func FromArticleEntities(articles []*entities.Article) []ArticleInfoResponse {
|
||||
if len(articles) == 0 {
|
||||
return []ArticleInfoResponse{}
|
||||
}
|
||||
|
||||
responses := make([]ArticleInfoResponse, len(articles))
|
||||
for i, article := range articles {
|
||||
if response := FromArticleEntity(article); response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
Reference in New Issue
Block a user