add article
This commit is contained in:
@@ -22,6 +22,9 @@ import (
|
||||
// 产品域实体
|
||||
productEntities "tyapi-server/internal/domains/product/entities"
|
||||
|
||||
// 文章域实体
|
||||
articleEntities "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
apiEntities "tyapi-server/internal/domains/api/entities"
|
||||
"tyapi-server/internal/infrastructure/database"
|
||||
)
|
||||
@@ -224,6 +227,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&financeEntities.AlipayOrder{},
|
||||
&financeEntities.InvoiceApplication{},
|
||||
&financeEntities.UserInvoiceInfo{},
|
||||
|
||||
// 产品域
|
||||
&productEntities.Product{},
|
||||
&productEntities.ProductPackageItem{},
|
||||
@@ -231,6 +235,12 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&productEntities.Subscription{},
|
||||
&productEntities.ProductDocumentation{},
|
||||
&productEntities.ProductApiConfig{},
|
||||
|
||||
// 文章域
|
||||
&articleEntities.Article{},
|
||||
&articleEntities.Category{},
|
||||
&articleEntities.Tag{},
|
||||
|
||||
// api
|
||||
&apiEntities.ApiUser{},
|
||||
&apiEntities.ApiCall{},
|
||||
@@ -312,3 +322,11 @@ func (a *Application) RunCommand(command string, args ...string) error {
|
||||
return fmt.Errorf("unknown command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
// GetArticleService 获取文章服务 (用于 Worker)
|
||||
func (app *Application) GetArticleService() interface{} {
|
||||
// 这里需要从容器中获取文章服务
|
||||
// 由于循环导入问题,暂时返回 nil
|
||||
// 实际使用时需要通过其他方式获取
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
@@ -9,11 +10,14 @@ import (
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/application/article"
|
||||
"tyapi-server/internal/application/certification"
|
||||
"tyapi-server/internal/application/finance"
|
||||
"tyapi-server/internal/application/product"
|
||||
"tyapi-server/internal/application/user"
|
||||
"tyapi-server/internal/config"
|
||||
domain_article_repo "tyapi-server/internal/domains/article/repositories"
|
||||
article_service "tyapi-server/internal/domains/article/services"
|
||||
domain_certification_repo "tyapi-server/internal/domains/certification/repositories"
|
||||
certification_service "tyapi-server/internal/domains/certification/services"
|
||||
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
|
||||
@@ -23,6 +27,7 @@ import (
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/infrastructure/cache"
|
||||
"tyapi-server/internal/infrastructure/database"
|
||||
article_repo "tyapi-server/internal/infrastructure/database/repositories/article"
|
||||
certification_repo "tyapi-server/internal/infrastructure/database/repositories/certification"
|
||||
finance_repo "tyapi-server/internal/infrastructure/database/repositories/finance"
|
||||
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
|
||||
@@ -38,6 +43,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
"tyapi-server/internal/infrastructure/http/routes"
|
||||
"tyapi-server/internal/infrastructure/task"
|
||||
shared_database "tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/esign"
|
||||
"tyapi-server/internal/shared/events"
|
||||
@@ -511,6 +517,25 @@ func NewContainer() *Container {
|
||||
),
|
||||
),
|
||||
|
||||
// 仓储层 - 文章域
|
||||
fx.Provide(
|
||||
// 文章仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
article_repo.NewGormArticleRepository,
|
||||
fx.As(new(domain_article_repo.ArticleRepository)),
|
||||
),
|
||||
// 分类仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
article_repo.NewGormCategoryRepository,
|
||||
fx.As(new(domain_article_repo.CategoryRepository)),
|
||||
),
|
||||
// 标签仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
article_repo.NewGormTagRepository,
|
||||
fx.As(new(domain_article_repo.TagRepository)),
|
||||
),
|
||||
),
|
||||
|
||||
// API域仓储层
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
@@ -576,6 +601,8 @@ func NewContainer() *Container {
|
||||
infra_events.NewInvoiceEventHandler,
|
||||
certification_service.NewCertificationAggregateService,
|
||||
certification_service.NewEnterpriseInfoSubmitRecordService,
|
||||
// 文章领域服务
|
||||
article_service.NewArticleService,
|
||||
),
|
||||
|
||||
// API域服务层
|
||||
@@ -591,6 +618,15 @@ func NewContainer() *Container {
|
||||
api_app.NewApiApplicationService,
|
||||
),
|
||||
|
||||
// 任务系统
|
||||
fx.Provide(
|
||||
// Asynq 客户端
|
||||
func(cfg *config.Config, logger *zap.Logger) *task.AsynqClient {
|
||||
redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port)
|
||||
return task.NewAsynqClient(redisAddr, logger)
|
||||
},
|
||||
),
|
||||
|
||||
// 应用服务
|
||||
fx.Provide(
|
||||
// 用户应用服务 - 绑定到接口
|
||||
@@ -642,6 +678,11 @@ func NewContainer() *Container {
|
||||
product.NewSubscriptionApplicationService,
|
||||
fx.As(new(product.SubscriptionApplicationService)),
|
||||
),
|
||||
// 文章应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
article.NewArticleApplicationService,
|
||||
fx.As(new(article.ArticleApplicationService)),
|
||||
),
|
||||
),
|
||||
|
||||
// HTTP处理器
|
||||
@@ -658,6 +699,15 @@ func NewContainer() *Container {
|
||||
handlers.NewProductAdminHandler,
|
||||
// API Handler
|
||||
handlers.NewApiHandler,
|
||||
// 文章HTTP处理器
|
||||
func(
|
||||
appService article.ArticleApplicationService,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
) *handlers.ArticleHandler {
|
||||
return handlers.NewArticleHandler(appService, responseBuilder, validator, logger)
|
||||
},
|
||||
),
|
||||
|
||||
// 路由注册
|
||||
@@ -673,6 +723,8 @@ func NewContainer() *Container {
|
||||
// 产品管理员路由
|
||||
routes.NewProductAdminRoutes,
|
||||
// API路由
|
||||
// 文章路由
|
||||
routes.NewArticleRoutes,
|
||||
routes.NewApiRoutes,
|
||||
),
|
||||
|
||||
@@ -766,6 +818,7 @@ func RegisterRoutes(
|
||||
financeRoutes *routes.FinanceRoutes,
|
||||
productRoutes *routes.ProductRoutes,
|
||||
productAdminRoutes *routes.ProductAdminRoutes,
|
||||
articleRoutes *routes.ArticleRoutes,
|
||||
apiRoutes *routes.ApiRoutes,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
@@ -781,6 +834,7 @@ func RegisterRoutes(
|
||||
financeRoutes.Register(router)
|
||||
productRoutes.Register(router)
|
||||
productAdminRoutes.Register(router)
|
||||
articleRoutes.Register(router)
|
||||
|
||||
// 打印注册的路由信息
|
||||
router.PrintRoutes()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"tyapi-server/internal/domains/api/services/processors/jrzq"
|
||||
"tyapi-server/internal/domains/api/services/processors/qcxg"
|
||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||
"tyapi-server/internal/domains/api/services/processors/test"
|
||||
"tyapi-server/internal/domains/api/services/processors/yysy"
|
||||
"tyapi-server/internal/domains/product/services"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
@@ -162,6 +163,11 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
|
||||
// FLXG系列处理器 - 风险管控 (包含原FXHY功能)
|
||||
"FLXG8B4D": flxg.ProcessFLXG8B4DRequest,
|
||||
|
||||
// TEST系列处理器 - 测试用处理器
|
||||
"TEST001": test.ProcessTestRequest,
|
||||
"TEST002": test.ProcessTestErrorRequest,
|
||||
"TEST003": test.ProcessTestTimeoutRequest,
|
||||
}
|
||||
|
||||
// 批量注册到组合包服务
|
||||
|
||||
94
internal/domains/api/services/processors/test/README.md
Normal file
94
internal/domains/api/services/processors/test/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 测试处理器使用说明
|
||||
|
||||
这个目录包含了用于测试的处理器,可以模拟各种API请求场景,帮助开发和测试人员验证系统功能。
|
||||
|
||||
## 处理器列表
|
||||
|
||||
### 1. ProcessTestRequest - 基础测试处理器
|
||||
- **功能**: 模拟正常的API请求处理
|
||||
- **用途**: 测试基本的请求处理流程、参数验证、响应生成等
|
||||
|
||||
#### 请求参数
|
||||
```json
|
||||
{
|
||||
"test_param": "测试参数值",
|
||||
"delay": 1000
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"message": "测试请求处理成功",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"request_id": "test_20240101120000_000000000",
|
||||
"test_param": "测试参数值",
|
||||
"process_time_ms": 1005,
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ProcessTestErrorRequest - 错误测试处理器
|
||||
- **功能**: 模拟各种错误情况
|
||||
- **用途**: 测试错误处理机制、异常响应等
|
||||
|
||||
#### 支持的错误类型
|
||||
- `system_error`: 系统错误
|
||||
- `datasource_error`: 数据源错误
|
||||
- `not_found`: 资源未找到
|
||||
- `invalid_param`: 参数无效
|
||||
|
||||
#### 请求示例
|
||||
```json
|
||||
{
|
||||
"test_param": "system_error"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ProcessTestTimeoutRequest - 超时测试处理器
|
||||
- **功能**: 模拟长时间处理导致的超时
|
||||
- **用途**: 测试超时处理、上下文取消等
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 开发阶段
|
||||
- 验证处理器框架是否正常工作
|
||||
- 测试参数验证逻辑
|
||||
- 验证错误处理机制
|
||||
|
||||
### 测试阶段
|
||||
- 性能测试(通过delay参数)
|
||||
- 超时测试
|
||||
- 错误场景测试
|
||||
- 集成测试
|
||||
|
||||
### 调试阶段
|
||||
- 快速验证API调用流程
|
||||
- 测试中间件功能
|
||||
- 验证日志记录
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **延迟参数**: `delay` 参数最大值为5000毫秒(5秒),避免测试时等待时间过长
|
||||
2. **上下文处理**: 所有处理器都正确处理上下文取消,支持超时控制
|
||||
3. **错误处理**: 遵循项目的错误处理规范,使用预定义的错误类型
|
||||
4. **参数验证**: 使用标准的参数验证机制,确保测试的真实性
|
||||
|
||||
## 集成到路由
|
||||
|
||||
要将测试处理器集成到API路由中,需要在相应的路由配置中添加:
|
||||
|
||||
```go
|
||||
// 在路由配置中添加测试端点
|
||||
router.POST("/api/test/basic", handlers.WrapProcessor(processors.ProcessTestRequest))
|
||||
router.POST("/api/test/error", handlers.WrapProcessor(processors.ProcessTestErrorRequest))
|
||||
router.POST("/api/test/timeout", handlers.WrapProcessor(processors.ProcessTestTimeoutRequest))
|
||||
```
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **基础功能测试**: 先使用 `ProcessTestRequest` 验证基本流程
|
||||
2. **错误场景测试**: 使用 `ProcessTestErrorRequest` 测试各种错误情况
|
||||
3. **性能测试**: 通过调整 `delay` 参数测试不同响应时间
|
||||
4. **超时测试**: 使用 `ProcessTestTimeoutRequest` 验证超时处理
|
||||
5. **压力测试**: 并发调用测试处理器的稳定性
|
||||
120
internal/domains/api/services/processors/test/test_processor.go
Normal file
120
internal/domains/api/services/processors/test/test_processor.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
)
|
||||
|
||||
// TestRequest 测试请求参数
|
||||
type TestRequest struct {
|
||||
TestParam string `json:"test_param" validate:"required"`
|
||||
Delay int `json:"delay" validate:"min=0,max=5000"` // 延迟毫秒数,最大5秒
|
||||
}
|
||||
|
||||
// TestResponse 测试响应数据
|
||||
type TestResponse struct {
|
||||
Message string `json:"message"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
RequestID string `json:"request_id"`
|
||||
TestParam string `json:"test_param"`
|
||||
ProcessTime int64 `json:"process_time_ms"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ProcessTestRequest 测试处理器,用于模拟API请求
|
||||
func ProcessTestRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 解析请求参数
|
||||
var req TestRequest
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if err := deps.Validator.ValidateStruct(req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 模拟处理延迟
|
||||
if req.Delay > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.Join(processors.ErrSystem, ctx.Err())
|
||||
case <-time.After(time.Duration(req.Delay) * time.Millisecond):
|
||||
// 延迟完成
|
||||
}
|
||||
}
|
||||
|
||||
// 检查上下文是否已取消
|
||||
if ctx.Err() != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, ctx.Err())
|
||||
}
|
||||
|
||||
// 生成响应数据
|
||||
response := TestResponse{
|
||||
Message: "测试请求处理成功",
|
||||
Timestamp: time.Now(),
|
||||
RequestID: generateTestRequestID(),
|
||||
TestParam: req.TestParam,
|
||||
ProcessTime: time.Since(startTime).Milliseconds(),
|
||||
Status: "success",
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
result, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ProcessTestErrorRequest 测试错误处理的处理器
|
||||
func ProcessTestErrorRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var req TestRequest
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 模拟不同类型的错误
|
||||
switch req.TestParam {
|
||||
case "system_error":
|
||||
return nil, processors.ErrSystem
|
||||
case "datasource_error":
|
||||
return nil, processors.ErrDatasource
|
||||
case "not_found":
|
||||
return nil, processors.ErrNotFound
|
||||
case "invalid_param":
|
||||
return nil, processors.ErrInvalidParam
|
||||
default:
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("未知错误类型"))
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessTestTimeoutRequest 测试超时处理的处理器
|
||||
func ProcessTestTimeoutRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var req TestRequest
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 模拟长时间处理
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.Join(processors.ErrSystem, ctx.Err())
|
||||
case <-time.After(10 * time.Second): // 10秒超时
|
||||
// 这里通常不会执行到,因为上下文会先超时
|
||||
}
|
||||
|
||||
return nil, processors.ErrSystem
|
||||
}
|
||||
|
||||
// generateTestRequestID 生成测试用的请求ID
|
||||
func generateTestRequestID() string {
|
||||
return "test_" + time.Now().Format("20060102150405") + "_" + time.Now().Format("000000000")
|
||||
}
|
||||
@@ -38,10 +38,10 @@ func ProcessYYSY6F2ERequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idNo": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"phoneType": paramsDto.MobileType,
|
||||
"name": encryptedName,
|
||||
"idNo": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"phoneType": paramsDto.MobileType,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,4 +55,4 @@ func ProcessYYSY6F2ERequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
195
internal/domains/article/entities/article.go
Normal file
195
internal/domains/article/entities/article.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ArticleStatus 文章状态枚举
|
||||
type ArticleStatus string
|
||||
|
||||
const (
|
||||
ArticleStatusDraft ArticleStatus = "draft" // 草稿
|
||||
ArticleStatusPublished ArticleStatus = "published" // 已发布
|
||||
ArticleStatusArchived ArticleStatus = "archived" // 已归档
|
||||
)
|
||||
|
||||
// Article 文章聚合根
|
||||
// 系统的核心内容实体,提供文章的完整生命周期管理
|
||||
// 支持草稿、发布、归档状态,实现Entity接口便于统一管理
|
||||
type Article struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"文章唯一标识"`
|
||||
Title string `gorm:"type:varchar(200);not null" json:"title" comment:"文章标题"`
|
||||
Content string `gorm:"type:text;not null" json:"content" comment:"文章内容"`
|
||||
Summary string `gorm:"type:varchar(500)" json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `gorm:"type:varchar(500)" json:"cover_image" comment:"封面图片"`
|
||||
|
||||
// 分类
|
||||
CategoryID string `gorm:"type:varchar(36)" json:"category_id" comment:"分类ID"`
|
||||
|
||||
// 状态管理
|
||||
Status ArticleStatus `gorm:"type:varchar(20);not null;default:'draft'" json:"status" comment:"文章状态"`
|
||||
IsFeatured bool `gorm:"default:false" json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
|
||||
// 统计信息
|
||||
ViewCount int `gorm:"default:0" json:"view_count" comment:"阅读量"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 关联关系
|
||||
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty" comment:"分类信息"`
|
||||
Tags []Tag `gorm:"many2many:article_tag_relations;" json:"tags,omitempty" comment:"标签列表"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Article) TableName() string {
|
||||
return "articles"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (a *Article) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (a *Article) GetID() string {
|
||||
return a.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (a *Article) GetCreatedAt() time.Time {
|
||||
return a.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (a *Article) GetUpdatedAt() time.Time {
|
||||
return a.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证文章信息
|
||||
// 检查文章必填字段是否完整,确保数据的有效性
|
||||
func (a *Article) Validate() error {
|
||||
if a.Title == "" {
|
||||
return NewValidationError("文章标题不能为空")
|
||||
}
|
||||
if a.Content == "" {
|
||||
return NewValidationError("文章内容不能为空")
|
||||
}
|
||||
|
||||
// 验证标题长度
|
||||
if len(a.Title) > 200 {
|
||||
return NewValidationError("文章标题不能超过200个字符")
|
||||
}
|
||||
|
||||
// 验证摘要长度
|
||||
if a.Summary != "" && len(a.Summary) > 500 {
|
||||
return NewValidationError("文章摘要不能超过500个字符")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Publish 发布文章
|
||||
func (a *Article) Publish() error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
|
||||
a.Status = ArticleStatusPublished
|
||||
now := time.Now()
|
||||
a.PublishedAt = &now
|
||||
a.ScheduledAt = nil // 清除定时发布时间
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchedulePublish 定时发布文章
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
|
||||
if scheduledTime.Before(time.Now()) {
|
||||
return NewValidationError("定时发布时间不能早于当前时间")
|
||||
}
|
||||
|
||||
a.Status = ArticleStatusDraft // 保持草稿状态,等待定时发布
|
||||
a.ScheduledAt = &scheduledTime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsScheduled 判断是否已设置定时发布
|
||||
func (a *Article) IsScheduled() bool {
|
||||
return a.ScheduledAt != nil && a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取定时发布时间
|
||||
func (a *Article) GetScheduledTime() *time.Time {
|
||||
return a.ScheduledAt
|
||||
}
|
||||
|
||||
// Archive 归档文章
|
||||
func (a *Article) Archive() error {
|
||||
if a.Status == ArticleStatusArchived {
|
||||
return NewValidationError("文章已经是归档状态")
|
||||
}
|
||||
|
||||
a.Status = ArticleStatusArchived
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementViewCount 增加阅读量
|
||||
func (a *Article) IncrementViewCount() {
|
||||
a.ViewCount++
|
||||
}
|
||||
|
||||
// SetFeatured 设置推荐状态
|
||||
func (a *Article) SetFeatured(featured bool) {
|
||||
a.IsFeatured = featured
|
||||
}
|
||||
|
||||
// IsPublished 判断是否已发布
|
||||
func (a *Article) IsPublished() bool {
|
||||
return a.Status == ArticleStatusPublished
|
||||
}
|
||||
|
||||
// IsDraft 判断是否为草稿
|
||||
func (a *Article) IsDraft() bool {
|
||||
return a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// IsArchived 判断是否已归档
|
||||
func (a *Article) IsArchived() bool {
|
||||
return a.Status == ArticleStatusArchived
|
||||
}
|
||||
|
||||
// CanEdit 判断是否可以编辑
|
||||
func (a *Article) CanEdit() bool {
|
||||
return a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// CanPublish 判断是否可以发布
|
||||
func (a *Article) CanPublish() bool {
|
||||
return a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// CanArchive 判断是否可以归档
|
||||
func (a *Article) CanArchive() bool {
|
||||
return a.Status == ArticleStatusPublished
|
||||
}
|
||||
78
internal/domains/article/entities/category.go
Normal file
78
internal/domains/article/entities/category.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Category 文章分类实体
|
||||
// 用于对文章进行分类管理,支持层级结构和排序
|
||||
type Category struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"分类唯一标识"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name" comment:"分类名称"`
|
||||
Description string `gorm:"type:text" json:"description" comment:"分类描述"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order" comment:"排序"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 关联关系
|
||||
Articles []Article `gorm:"foreignKey:CategoryID" json:"articles,omitempty" comment:"分类下的文章"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Category) TableName() string {
|
||||
return "article_categories"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (c *Category) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == "" {
|
||||
c.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (c *Category) GetID() string {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (c *Category) GetCreatedAt() time.Time {
|
||||
return c.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (c *Category) GetUpdatedAt() time.Time {
|
||||
return c.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证分类信息
|
||||
// 检查分类必填字段是否完整,确保数据的有效性
|
||||
func (c *Category) Validate() error {
|
||||
if c.Name == "" {
|
||||
return NewValidationError("分类名称不能为空")
|
||||
}
|
||||
|
||||
// 验证名称长度
|
||||
if len(c.Name) > 100 {
|
||||
return NewValidationError("分类名称不能超过100个字符")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSortOrder 设置排序
|
||||
func (c *Category) SetSortOrder(order int) {
|
||||
c.SortOrder = order
|
||||
}
|
||||
21
internal/domains/article/entities/errors.go
Normal file
21
internal/domains/article/entities/errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package entities
|
||||
|
||||
// ValidationError 验证错误
|
||||
type ValidationError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewValidationError 创建验证错误
|
||||
func NewValidationError(message string) *ValidationError {
|
||||
return &ValidationError{Message: message}
|
||||
}
|
||||
|
||||
// IsValidationError 判断是否为验证错误
|
||||
func IsValidationError(err error) bool {
|
||||
_, ok := err.(*ValidationError)
|
||||
return ok
|
||||
}
|
||||
102
internal/domains/article/entities/tag.go
Normal file
102
internal/domains/article/entities/tag.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tag 文章标签实体
|
||||
// 用于对文章进行标签化管理,支持颜色配置
|
||||
type Tag struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"标签唯一标识"`
|
||||
Name string `gorm:"type:varchar(50);not null" json:"name" comment:"标签名称"`
|
||||
Color string `gorm:"type:varchar(20);default:'#1890ff'" json:"color" comment:"标签颜色"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 关联关系
|
||||
Articles []Article `gorm:"many2many:article_tag_relations;" json:"articles,omitempty" comment:"标签下的文章"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Tag) TableName() string {
|
||||
return "article_tags"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (t *Tag) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.ID == "" {
|
||||
t.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (t *Tag) GetID() string {
|
||||
return t.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (t *Tag) GetCreatedAt() time.Time {
|
||||
return t.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (t *Tag) GetUpdatedAt() time.Time {
|
||||
return t.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证标签信息
|
||||
// 检查标签必填字段是否完整,确保数据的有效性
|
||||
func (t *Tag) Validate() error {
|
||||
if t.Name == "" {
|
||||
return NewValidationError("标签名称不能为空")
|
||||
}
|
||||
|
||||
// 验证名称长度
|
||||
if len(t.Name) > 50 {
|
||||
return NewValidationError("标签名称不能超过50个字符")
|
||||
}
|
||||
|
||||
// 验证颜色格式
|
||||
if t.Color != "" && !isValidColor(t.Color) {
|
||||
return NewValidationError("标签颜色格式无效")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetColor 设置标签颜色
|
||||
func (t *Tag) SetColor(color string) error {
|
||||
if color != "" && !isValidColor(color) {
|
||||
return NewValidationError("标签颜色格式无效")
|
||||
}
|
||||
t.Color = color
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidColor 验证颜色格式
|
||||
func isValidColor(color string) bool {
|
||||
// 简单的颜色格式验证,支持 #RRGGBB 格式
|
||||
if len(color) == 7 && color[0] == '#' {
|
||||
for i := 1; i < 7; i++ {
|
||||
if !((color[i] >= '0' && color[i] <= '9') ||
|
||||
(color[i] >= 'a' && color[i] <= 'f') ||
|
||||
(color[i] >= 'A' && color[i] <= 'F')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories/queries"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// ArticleRepository 文章仓储接口
|
||||
type ArticleRepository interface {
|
||||
interfaces.Repository[entities.Article]
|
||||
|
||||
// 自定义查询方法
|
||||
FindByAuthorID(ctx context.Context, authorID string) ([]*entities.Article, error)
|
||||
FindByCategoryID(ctx context.Context, categoryID string) ([]*entities.Article, error)
|
||||
FindByStatus(ctx context.Context, status entities.ArticleStatus) ([]*entities.Article, error)
|
||||
FindFeatured(ctx context.Context) ([]*entities.Article, error)
|
||||
Search(ctx context.Context, query *queries.SearchArticleQuery) ([]*entities.Article, int64, error)
|
||||
ListArticles(ctx context.Context, query *queries.ListArticleQuery) ([]*entities.Article, int64, error)
|
||||
|
||||
// 统计方法
|
||||
CountByCategoryID(ctx context.Context, categoryID string) (int64, error)
|
||||
CountByStatus(ctx context.Context, status entities.ArticleStatus) (int64, error)
|
||||
|
||||
// 更新统计信息
|
||||
IncrementViewCount(ctx context.Context, articleID string) error
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// CategoryRepository 分类仓储接口
|
||||
type CategoryRepository interface {
|
||||
interfaces.Repository[entities.Category]
|
||||
|
||||
// 自定义查询方法
|
||||
FindActive(ctx context.Context) ([]*entities.Category, error)
|
||||
FindBySortOrder(ctx context.Context) ([]*entities.Category, error)
|
||||
|
||||
// 统计方法
|
||||
CountActive(ctx context.Context) (int64, error)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package queries
|
||||
|
||||
import "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
// ListArticleQuery 文章列表查询
|
||||
type ListArticleQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Status entities.ArticleStatus `json:"status"`
|
||||
CategoryID string `json:"category_id"`
|
||||
TagID string `json:"tag_id"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
IsFeatured *bool `json:"is_featured"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDir string `json:"order_dir"`
|
||||
}
|
||||
|
||||
// SearchArticleQuery 文章搜索查询
|
||||
type SearchArticleQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Keyword string `json:"keyword"`
|
||||
CategoryID string `json:"category_id"`
|
||||
AuthorID string `json:"author_id"`
|
||||
Status entities.ArticleStatus `json:"status"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDir string `json:"order_dir"`
|
||||
}
|
||||
|
||||
// GetArticleQuery 获取文章详情查询
|
||||
type GetArticleQuery struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// GetArticlesByAuthorQuery 获取作者文章查询
|
||||
type GetArticlesByAuthorQuery struct {
|
||||
AuthorID string `json:"author_id"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// GetArticlesByCategoryQuery 获取分类文章查询
|
||||
type GetArticlesByCategoryQuery struct {
|
||||
CategoryID string `json:"category_id"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// TagRepository 标签仓储接口
|
||||
type TagRepository interface {
|
||||
interfaces.Repository[entities.Tag]
|
||||
|
||||
// 自定义查询方法
|
||||
FindByArticleID(ctx context.Context, articleID string) ([]*entities.Tag, error)
|
||||
FindByName(ctx context.Context, name string) (*entities.Tag, error)
|
||||
|
||||
// 关联方法
|
||||
AddTagToArticle(ctx context.Context, articleID string, tagID string) error
|
||||
RemoveTagFromArticle(ctx context.Context, articleID string, tagID string) error
|
||||
GetArticleTags(ctx context.Context, articleID string) ([]*entities.Tag, error)
|
||||
}
|
||||
94
internal/domains/article/services/article_service.go
Normal file
94
internal/domains/article/services/article_service.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
)
|
||||
|
||||
// ArticleService 文章领域服务
|
||||
// 处理文章相关的业务逻辑,包括验证、状态管理等
|
||||
type ArticleService struct{}
|
||||
|
||||
// NewArticleService 创建文章领域服务
|
||||
func NewArticleService() *ArticleService {
|
||||
return &ArticleService{}
|
||||
}
|
||||
|
||||
// ValidateArticle 验证文章
|
||||
// 检查文章是否符合业务规则
|
||||
func (s *ArticleService) ValidateArticle(article *entities.Article) error {
|
||||
// 1. 基础验证
|
||||
if err := article.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 业务规则验证
|
||||
// 标题不能包含敏感词
|
||||
if s.containsSensitiveWords(article.Title) {
|
||||
return entities.NewValidationError("文章标题包含敏感词")
|
||||
}
|
||||
|
||||
// 内容不能包含敏感词
|
||||
if s.containsSensitiveWords(article.Content) {
|
||||
return entities.NewValidationError("文章内容包含敏感词")
|
||||
}
|
||||
|
||||
// 摘要长度不能超过内容长度
|
||||
if article.Summary != "" && len(article.Summary) >= len(article.Content) {
|
||||
return entities.NewValidationError("文章摘要不能超过内容长度")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanPublish 检查是否可以发布
|
||||
func (s *ArticleService) CanPublish(article *entities.Article) error {
|
||||
if !article.CanPublish() {
|
||||
return entities.NewValidationError("文章状态不允许发布")
|
||||
}
|
||||
|
||||
// 检查必填字段
|
||||
if article.Title == "" {
|
||||
return entities.NewValidationError("文章标题不能为空")
|
||||
}
|
||||
if article.Content == "" {
|
||||
return entities.NewValidationError("文章内容不能为空")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanEdit 检查是否可以编辑
|
||||
func (s *ArticleService) CanEdit(article *entities.Article) error {
|
||||
if !article.CanEdit() {
|
||||
return entities.NewValidationError("文章状态不允许编辑")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsSensitiveWords 检查是否包含敏感词
|
||||
func (s *ArticleService) containsSensitiveWords(text string) bool {
|
||||
// TODO: 实现敏感词检查逻辑
|
||||
// 这里可以集成敏感词库或调用外部服务
|
||||
sensitiveWords := []string{
|
||||
"敏感词1",
|
||||
"敏感词2",
|
||||
"敏感词3",
|
||||
}
|
||||
|
||||
for _, word := range sensitiveWords {
|
||||
if len(word) > 0 && len(text) > 0 {
|
||||
// 简单的字符串包含检查
|
||||
// 实际项目中应该使用更复杂的算法
|
||||
if len(text) >= len(word) {
|
||||
for i := 0; i <= len(text)-len(word); i++ {
|
||||
if text[i:i+len(word)] == word {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
43
internal/infrastructure/cache/redis_cache.go
vendored
43
internal/infrastructure/cache/redis_cache.go
vendored
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 == "" {
|
||||
|
||||
650
internal/infrastructure/http/handlers/article_handler.go
Normal file
650
internal/infrastructure/http/handlers/article_handler.go
Normal 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, "标签删除成功")
|
||||
}
|
||||
@@ -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签宝回调请求",
|
||||
|
||||
@@ -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("🔍 请求事件系统调试信息")
|
||||
|
||||
|
||||
@@ -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{} "服务器内部错误"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
104
internal/infrastructure/http/routes/article_routes.go
Normal file
104
internal/infrastructure/http/routes/article_routes.go
Normal 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("文章路由注册完成")
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
58
internal/infrastructure/task/article_task_handler.go
Normal file
58
internal/infrastructure/task/article_task_handler.go
Normal 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
|
||||
}
|
||||
75
internal/infrastructure/task/asynq_client.go
Normal file
75
internal/infrastructure/task/asynq_client.go
Normal 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
|
||||
}
|
||||
98
internal/infrastructure/task/asynq_worker.go
Normal file
98
internal/infrastructure/task/asynq_worker.go
Normal 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...))
|
||||
}
|
||||
7
internal/infrastructure/task/task_types.go
Normal file
7
internal/infrastructure/task/task_types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package task
|
||||
|
||||
// 任务类型常量
|
||||
const (
|
||||
// TaskTypeArticlePublish 文章定时发布任务
|
||||
TaskTypeArticlePublish = "article:publish"
|
||||
)
|
||||
Reference in New Issue
Block a user