355 lines
8.4 KiB
Go
355 lines
8.4 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"cms-api/internal/dao"
|
|||
|
|
"cms-api/internal/model"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|||
|
|
"github.com/gogf/gf/v2/frame/g"
|
|||
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|||
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type sArticle struct{}
|
|||
|
|
|
|||
|
|
func Article() *sArticle {
|
|||
|
|
return &sArticle{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetById 根据ID获取文章信息
|
|||
|
|
func (s *sArticle) GetById(ctx context.Context, id int) (*model.Article, error) {
|
|||
|
|
article, err := dao.Article.GetById(ctx, id)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if article != nil {
|
|||
|
|
// 增加浏览次数
|
|||
|
|
go func() {
|
|||
|
|
dao.Article.IncrementViewCount(context.Background(), id)
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
return article, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetBySlug 根据URL别名获取文章
|
|||
|
|
func (s *sArticle) GetBySlug(ctx context.Context, slug string) (*model.Article, error) {
|
|||
|
|
article, err := dao.Article.GetBySlug(ctx, slug)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if article != nil {
|
|||
|
|
// 增加浏览次数
|
|||
|
|
go func() {
|
|||
|
|
dao.Article.IncrementViewCount(context.Background(), article.Id)
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
return article, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// List 获取文章列表
|
|||
|
|
func (s *sArticle) List(ctx context.Context, req *model.ArticleListRequest) (*model.PageResponse, error) {
|
|||
|
|
var (
|
|||
|
|
page = req.Page
|
|||
|
|
pageSize = req.PageSize
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if page <= 0 {
|
|||
|
|
page = 1
|
|||
|
|
}
|
|||
|
|
if pageSize <= 0 {
|
|||
|
|
pageSize = 10
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置分页参数
|
|||
|
|
req.Page = page
|
|||
|
|
req.PageSize = pageSize
|
|||
|
|
|
|||
|
|
// 获取列表
|
|||
|
|
articles, total, err := dao.Article.List(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &model.PageResponse{
|
|||
|
|
List: articles,
|
|||
|
|
Total: total,
|
|||
|
|
Page: page,
|
|||
|
|
PageSize: pageSize,
|
|||
|
|
TotalPages: (total + pageSize - 1) / pageSize,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Create 创建文章
|
|||
|
|
func (s *sArticle) Create(ctx context.Context, req *model.CreateArticleRequest) error {
|
|||
|
|
// 检查URL别名是否存在
|
|||
|
|
if req.Slug != "" {
|
|||
|
|
existArticle, err := dao.Article.GetBySlug(ctx, req.Slug)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if existArticle != nil {
|
|||
|
|
return gerror.New("URL别名已存在")
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 如果没有提供slug,则根据标题生成
|
|||
|
|
req.Slug = s.generateSlug(req.Title)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取当前用户ID
|
|||
|
|
authorId := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("admin_id"))
|
|||
|
|
if authorId == 0 {
|
|||
|
|
authorId = gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("user_id"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理HTML内容(如果是Markdown,需要转换)
|
|||
|
|
htmlContent := req.Content
|
|||
|
|
if req.Content != "" {
|
|||
|
|
// 这里可以添加Markdown到HTML的转换逻辑
|
|||
|
|
htmlContent = s.markdownToHTML(req.Content)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建文章
|
|||
|
|
article := &model.Article{
|
|||
|
|
Title: req.Title,
|
|||
|
|
Slug: req.Slug,
|
|||
|
|
Summary: req.Summary,
|
|||
|
|
Content: req.Content,
|
|||
|
|
HtmlContent: htmlContent,
|
|||
|
|
CoverImage: req.CoverImage,
|
|||
|
|
CategoryId: req.CategoryId,
|
|||
|
|
Tags: req.Tags,
|
|||
|
|
AuthorId: authorId,
|
|||
|
|
IsPublished: req.IsPublished,
|
|||
|
|
IsFeatured: req.IsFeatured,
|
|||
|
|
IsTop: req.IsTop,
|
|||
|
|
SeoTitle: req.SeoTitle,
|
|||
|
|
SeoDescription: req.SeoDescription,
|
|||
|
|
SeoKeywords: req.SeoKeywords,
|
|||
|
|
CreatedAt: gtime.Now(),
|
|||
|
|
UpdatedAt: gtime.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if req.IsPublished == 1 {
|
|||
|
|
article.PublishedAt = gtime.Now()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_, err := dao.Article.Create(ctx, article)
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update 更新文章
|
|||
|
|
func (s *sArticle) Update(ctx context.Context, id int, req *model.UpdateArticleRequest) error {
|
|||
|
|
// 检查文章是否存在
|
|||
|
|
article, err := s.GetById(ctx, id)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if article == nil {
|
|||
|
|
return gerror.New("文章不存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查URL别名是否被其他文章使用
|
|||
|
|
if req.Slug != "" && req.Slug != article.Slug {
|
|||
|
|
existArticle, err := dao.Article.GetBySlug(ctx, req.Slug)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if existArticle != nil && existArticle.Id != id {
|
|||
|
|
return gerror.New("URL别名已被使用")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理HTML内容
|
|||
|
|
htmlContent := req.Content
|
|||
|
|
if req.Content != "" {
|
|||
|
|
htmlContent = s.markdownToHTML(req.Content)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新文章
|
|||
|
|
updateData := g.Map{
|
|||
|
|
"title": req.Title,
|
|||
|
|
"slug": req.Slug,
|
|||
|
|
"summary": req.Summary,
|
|||
|
|
"content": req.Content,
|
|||
|
|
"html_content": htmlContent,
|
|||
|
|
"cover_image": req.CoverImage,
|
|||
|
|
"category_id": req.CategoryId,
|
|||
|
|
"tags": req.Tags,
|
|||
|
|
"is_published": req.IsPublished,
|
|||
|
|
"is_featured": req.IsFeatured,
|
|||
|
|
"is_top": req.IsTop,
|
|||
|
|
"seo_title": req.SeoTitle,
|
|||
|
|
"seo_description": req.SeoDescription,
|
|||
|
|
"seo_keywords": req.SeoKeywords,
|
|||
|
|
"updated_at": gtime.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果从草稿变为发布状态,设置发布时间
|
|||
|
|
if req.IsPublished == 1 && article.IsPublished == 0 {
|
|||
|
|
updateData["published_at"] = gtime.Now()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return dao.Article.Update(ctx, id, updateData)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Delete 删除文章
|
|||
|
|
func (s *sArticle) Delete(ctx context.Context, id int) error {
|
|||
|
|
// 检查文章是否存在
|
|||
|
|
article, err := s.GetById(ctx, id)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if article == nil {
|
|||
|
|
return gerror.New("文章不存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return dao.Article.Delete(ctx, id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UpdateStatus 更新文章状态
|
|||
|
|
func (s *sArticle) UpdateStatus(ctx context.Context, id int, isPublished int) error {
|
|||
|
|
// 检查文章是否存在
|
|||
|
|
article, err := s.GetById(ctx, id)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if article == nil {
|
|||
|
|
return gerror.New("文章不存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateData := g.Map{
|
|||
|
|
"is_published": isPublished,
|
|||
|
|
"updated_at": gtime.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是发布状态,设置发布时间
|
|||
|
|
if isPublished == 1 && article.IsPublished == 0 {
|
|||
|
|
updateData["published_at"] = gtime.Now()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return dao.Article.Update(ctx, id, updateData)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetFeatured 设置推荐状态
|
|||
|
|
func (s *sArticle) SetFeatured(ctx context.Context, id int, isFeatured int) error {
|
|||
|
|
// 检查文章是否存在
|
|||
|
|
article, err := s.GetById(ctx, id)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if article == nil {
|
|||
|
|
return gerror.New("文章不存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return dao.Article.Update(ctx, id, g.Map{
|
|||
|
|
"is_featured": isFeatured,
|
|||
|
|
"updated_at": gtime.Now(),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetTop 设置置顶状态
|
|||
|
|
func (s *sArticle) SetTop(ctx context.Context, id int, isTop int) error {
|
|||
|
|
// 检查文章是否存在
|
|||
|
|
article, err := s.GetById(ctx, id)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if article == nil {
|
|||
|
|
return gerror.New("文章不存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return dao.Article.Update(ctx, id, g.Map{
|
|||
|
|
"is_top": isTop,
|
|||
|
|
"updated_at": gtime.Now(),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStats 获取文章统计信息
|
|||
|
|
func (s *sArticle) GetStats(ctx context.Context) (map[string]interface{}, error) {
|
|||
|
|
// 获取总文章数
|
|||
|
|
totalCount, err := dao.Article.GetCount(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取已发布文章数
|
|||
|
|
publishedCount, err := dao.Article.GetPublishedCount(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取草稿数
|
|||
|
|
draftCount := totalCount - publishedCount
|
|||
|
|
|
|||
|
|
// 获取推荐文章数
|
|||
|
|
featuredCount, err := dao.Article.GetFeaturedCount(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"total_count": totalCount,
|
|||
|
|
"published_count": publishedCount,
|
|||
|
|
"draft_count": draftCount,
|
|||
|
|
"featured_count": featuredCount,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetFeatured 获取推荐文章
|
|||
|
|
func (s *sArticle) GetFeatured(ctx context.Context, limit int) ([]*model.Article, error) {
|
|||
|
|
if limit <= 0 {
|
|||
|
|
limit = 5
|
|||
|
|
}
|
|||
|
|
return dao.Article.GetFeatured(ctx, limit)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetLatest 获取最新文章
|
|||
|
|
func (s *sArticle) GetLatest(ctx context.Context, limit int) ([]*model.Article, error) {
|
|||
|
|
if limit <= 0 {
|
|||
|
|
limit = 10
|
|||
|
|
}
|
|||
|
|
return dao.Article.GetLatest(ctx, limit)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// generateSlug 生成URL别名
|
|||
|
|
func (s *sArticle) generateSlug(title string) string {
|
|||
|
|
// 简单的slug生成逻辑,实际项目中可能需要更复杂的处理
|
|||
|
|
slug := strings.ToLower(title)
|
|||
|
|
slug = strings.ReplaceAll(slug, " ", "-")
|
|||
|
|
slug = strings.ReplaceAll(slug, " ", "-") // 全角空格
|
|||
|
|
// 移除特殊字符,只保留字母、数字、中文和连字符
|
|||
|
|
// 这里简化处理,实际项目中可能需要更完善的slug生成逻辑
|
|||
|
|
return slug
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// markdownToHTML 将Markdown转换为HTML
|
|||
|
|
func (s *sArticle) markdownToHTML(markdown string) string {
|
|||
|
|
// 这里应该使用Markdown解析库,如github.com/russross/blackfriday
|
|||
|
|
// 暂时直接返回原内容,实际项目中需要实现Markdown解析
|
|||
|
|
return markdown
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Search 搜索文章
|
|||
|
|
func (s *sArticle) Search(ctx context.Context, keyword string, page, pageSize int) (*model.PageResponse, error) {
|
|||
|
|
if page <= 0 {
|
|||
|
|
page = 1
|
|||
|
|
}
|
|||
|
|
if pageSize <= 0 {
|
|||
|
|
pageSize = 10
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
articles, total, err := dao.Article.Search(ctx, keyword, page, pageSize)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &model.PageResponse{
|
|||
|
|
List: articles,
|
|||
|
|
Total: total,
|
|||
|
|
Page: page,
|
|||
|
|
PageSize: pageSize,
|
|||
|
|
TotalPages: (total + pageSize - 1) / pageSize,
|
|||
|
|
}, nil
|
|||
|
|
}
|