Files

441 lines
13 KiB
Go

package admin
import (
"context"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
"github.com/qianfree/team-api/internal/model/do"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// ============================================================
// 帮助分类 CRUD
// ============================================================
// CreateHelpCategory 创建帮助分类
func (s *sAdmin) CreateHelpCategory(ctx context.Context, req *v1.HelpCategoryCreateReq) (*v1.HelpCategoryCreateRes, error) {
// 检查 slug 唯一性
count, err := dao.SptCategories.Ctx(ctx).Where("slug", req.Slug).Count()
if err != nil {
return nil, err
}
if count > 0 {
return nil, common.NewBusinessError(consts.CodeHelpCategorySlugExists, consts.MsgHelpCategorySlugExists)
}
// 父分类校验:必须存在、必须是顶级分类(仅支持两级)
if req.ParentId > 0 {
if err = s.validateHelpCategoryParent(ctx, 0, req.ParentId); err != nil {
return nil, err
}
}
isVisible := true
if req.IsVisible != nil {
isVisible = *req.IsVisible
}
result, err := dao.SptCategories.Ctx(ctx).Data(do.SptCategories{
ParentId: req.ParentId,
Name: req.Name,
Slug: req.Slug,
Description: req.Description,
SortOrder: req.SortOrder,
Icon: req.Icon,
IsVisible: isVisible,
}).Insert()
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
return &v1.HelpCategoryCreateRes{Id: id}, nil
}
// UpdateHelpCategory 更新帮助分类
func (s *sAdmin) UpdateHelpCategory(ctx context.Context, req *v1.HelpCategoryUpdateReq) (*v1.HelpCategoryUpdateRes, error) {
var cat *struct {
Id int64 `json:"id"`
}
err := dao.SptCategories.Ctx(ctx).Where("id", req.Id).Scan(&cat)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if cat == nil {
return nil, common.NewBusinessError(consts.CodeHelpCategoryNotFound, consts.MsgHelpCategoryNotFound)
}
// 检查 slug 唯一性(排除自身)
count, err := dao.SptCategories.Ctx(ctx).Where("slug", req.Slug).WhereNot("id", req.Id).Count()
if err != nil {
return nil, err
}
if count > 0 {
return nil, common.NewBusinessError(consts.CodeHelpCategorySlugExists, consts.MsgHelpCategorySlugExists)
}
// 不允许将分类设置为自己的子分类
if req.ParentId > 0 && req.ParentId == req.Id {
return nil, common.NewBusinessError(consts.CodeBadRequest, "不能将分类设置为自己的子分类")
}
// 父分类校验:必须存在、必须是顶级分类(仅支持两级),且祖先链中不能出现自己(防环导致分类互相锁定无法删除)
if req.ParentId > 0 {
if err = s.validateHelpCategoryParent(ctx, req.Id, req.ParentId); err != nil {
return nil, err
}
}
updateData := do.SptCategories{
ParentId: req.ParentId,
Name: req.Name,
Slug: req.Slug,
Description: req.Description,
SortOrder: req.SortOrder,
Icon: req.Icon,
}
if req.IsVisible != nil {
updateData.IsVisible = *req.IsVisible
}
_, err = dao.SptCategories.Ctx(ctx).
Where("id", req.Id).
Data(updateData).
Update()
if err != nil {
return nil, err
}
return &v1.HelpCategoryUpdateRes{}, nil
}
// validateHelpCategoryParent 校验父分类:必须存在、必须是顶级分类(仅支持两级结构),
// 且父分类的祖先链中不能出现 selfId(否则会形成 A→B→A 的环,环上分类因互为父子被删除校验拦截,永远无法删除)
func (s *sAdmin) validateHelpCategoryParent(ctx context.Context, selfId, parentId int64) error {
var parent *struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
}
err := dao.SptCategories.Ctx(ctx).Where("id", parentId).Scan(&parent)
if err = common.IgnoreScanNoRows(err); err != nil {
return err
}
if parent == nil {
return common.NewBusinessError(consts.CodeHelpCategoryNotFound, consts.MsgHelpCategoryNotFound)
}
if parent.ParentId != 0 {
return common.NewBusinessError(consts.CodeBadRequest, "仅支持两级分类,父分类必须是顶级分类")
}
// 沿祖先链向上检查是否会出现自己(数据异常时用深度上限兜底)
current := parent.ParentId
for i := 0; i < 50 && current != 0; i++ {
if current == selfId {
return common.NewBusinessError(consts.CodeBadRequest, "不能将分类挂到自身或其子分类下")
}
var ancestor *struct {
ParentId int64 `json:"parent_id"`
}
err = dao.SptCategories.Ctx(ctx).Where("id", current).Scan(&ancestor)
if err = common.IgnoreScanNoRows(err); err != nil {
return err
}
if ancestor == nil {
return nil
}
current = ancestor.ParentId
}
return nil
}
// DeleteHelpCategory 删除帮助分类
func (s *sAdmin) DeleteHelpCategory(ctx context.Context, req *v1.HelpCategoryDeleteReq) (*v1.HelpCategoryDeleteRes, error) {
var cat *struct {
Id int64 `json:"id"`
}
err := dao.SptCategories.Ctx(ctx).Where("id", req.Id).Scan(&cat)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if cat == nil {
return nil, common.NewBusinessError(consts.CodeHelpCategoryNotFound, consts.MsgHelpCategoryNotFound)
}
// 检查是否有子分类
childCount, err := dao.SptCategories.Ctx(ctx).Where("parent_id", req.Id).Count()
if err != nil {
return nil, err
}
if childCount > 0 {
return nil, common.NewBusinessError(consts.CodeBadRequest, "该分类下有子分类,请先删除子分类")
}
// 检查分类下是否有文章
articleCount, err := dao.SptArticles.Ctx(ctx).Where("category_id", req.Id).Count()
if err != nil {
return nil, err
}
if articleCount > 0 {
return nil, common.NewBusinessError(consts.CodeBadRequest, "该分类下有文章,请先删除或移动文章")
}
_, err = dao.SptCategories.Ctx(ctx).Where("id", req.Id).Delete()
if err != nil {
return nil, err
}
return &v1.HelpCategoryDeleteRes{}, nil
}
// ListHelpCategories 帮助分类列表(管理后台)
func (s *sAdmin) ListHelpCategories(ctx context.Context, req *v1.HelpCategoryListReq) (*v1.HelpCategoryListRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
query := dao.SptCategories.Ctx(ctx)
if req.ParentId >= 0 {
query = query.Where("parent_id", req.ParentId)
}
var total int
rows := make([]*v1.HelpCategoryItem, 0)
err := query.OrderAsc("sort_order").OrderDesc("created_at").
Page(page, pageSize).
ScanAndCount(&rows, &total, false)
if err != nil {
return nil, err
}
return &v1.HelpCategoryListRes{
List: rows,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// ============================================================
// 帮助文章 CRUD
// ============================================================
// CreateHelpArticle 创建帮助文章
func (s *sAdmin) CreateHelpArticle(ctx context.Context, req *v1.HelpArticleCreateReq) (*v1.HelpArticleCreateRes, error) {
// 检查分类是否存在
var cat *struct {
Id int64 `json:"id"`
}
err := dao.SptCategories.Ctx(ctx).Where("id", req.CategoryId).Scan(&cat)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if cat == nil {
return nil, common.NewBusinessError(consts.CodeHelpCategoryNotFound, consts.MsgHelpCategoryNotFound)
}
// 检查 slug 唯一性
count, err := dao.SptArticles.Ctx(ctx).Where("slug", req.Slug).Count()
if err != nil {
return nil, err
}
if count > 0 {
return nil, common.NewBusinessError(consts.CodeHelpArticleSlugExists, consts.MsgHelpArticleSlugExists)
}
data := do.SptArticles{
CategoryId: req.CategoryId,
Title: req.Title,
Slug: req.Slug,
Content: req.Content,
Summary: req.Summary,
Status: req.Status,
AuthorId: common.GetCtxUserID(ctx),
SortOrder: req.SortOrder,
Keywords: req.Keywords,
}
if req.Status == "published" {
data.PublishedAt = gtime.Now()
}
result, err := dao.SptArticles.Ctx(ctx).Data(data).Insert()
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
// 更新分类的文章计数
s.refreshCategoryArticleCount(ctx, req.CategoryId)
return &v1.HelpArticleCreateRes{Id: id}, nil
}
// UpdateHelpArticle 更新帮助文章
func (s *sAdmin) UpdateHelpArticle(ctx context.Context, req *v1.HelpArticleUpdateReq) (*v1.HelpArticleUpdateRes, error) {
var article *struct {
Id int64 `json:"id"`
CategoryId int64 `json:"category_id"`
Status string `json:"status"`
PublishedAt *gtime.Time `json:"published_at"`
}
err := dao.SptArticles.Ctx(ctx).Where("id", req.Id).Scan(&article)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if article == nil {
return nil, common.NewBusinessError(consts.CodeHelpArticleNotFound, consts.MsgHelpArticleNotFound)
}
// 检查 slug 唯一性(排除自身)
count, err := dao.SptArticles.Ctx(ctx).Where("slug", req.Slug).WhereNot("id", req.Id).Count()
if err != nil {
return nil, err
}
if count > 0 {
return nil, common.NewBusinessError(consts.CodeHelpArticleSlugExists, consts.MsgHelpArticleSlugExists)
}
// 检查分类是否存在
var cat *struct {
Id int64 `json:"id"`
}
err = dao.SptCategories.Ctx(ctx).Where("id", req.CategoryId).Scan(&cat)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if cat == nil {
return nil, common.NewNotFoundError("分类")
}
data := do.SptArticles{
CategoryId: req.CategoryId,
Title: req.Title,
Slug: req.Slug,
Summary: req.Summary,
Status: req.Status,
SortOrder: req.SortOrder,
Keywords: req.Keywords,
}
if req.Content != "" {
data.Content = req.Content
}
// 从草稿发布时设置发布时间(仅首次发布固定;下架再上架不重置,保持原始发布时间与排序位置)
if req.Status == "published" && article.Status != "published" && article.PublishedAt == nil {
data.PublishedAt = gtime.Now()
}
_, err = dao.SptArticles.Ctx(ctx).
Where("id", req.Id).
Data(data).
Update()
if err != nil {
return nil, err
}
// 更新旧分类和新分类的文章计数
if article.CategoryId != req.CategoryId {
s.refreshCategoryArticleCount(ctx, article.CategoryId)
s.refreshCategoryArticleCount(ctx, req.CategoryId)
}
// 状态变更(草稿↔已发布)也需要刷新计数
if article.Status != req.Status {
s.refreshCategoryArticleCount(ctx, req.CategoryId)
}
return &v1.HelpArticleUpdateRes{}, nil
}
// DeleteHelpArticle 删除帮助文章
func (s *sAdmin) DeleteHelpArticle(ctx context.Context, req *v1.HelpArticleDeleteReq) (*v1.HelpArticleDeleteRes, error) {
var article *struct {
Id int64 `json:"id"`
CategoryId int64 `json:"category_id"`
}
err := dao.SptArticles.Ctx(ctx).Where("id", req.Id).Scan(&article)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if article == nil {
return nil, common.NewBusinessError(consts.CodeHelpArticleNotFound, consts.MsgHelpArticleNotFound)
}
_, err = dao.SptArticles.Ctx(ctx).Where("id", req.Id).Delete()
if err != nil {
return nil, err
}
// 更新分类的文章计数
s.refreshCategoryArticleCount(ctx, article.CategoryId)
return &v1.HelpArticleDeleteRes{}, nil
}
// ListHelpArticles 帮助文章列表(管理后台)
func (s *sAdmin) ListHelpArticles(ctx context.Context, req *v1.HelpArticleListReq) (*v1.HelpArticleListRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
query := dao.SptArticles.Ctx(ctx)
if req.CategoryId > 0 {
query = query.Where("category_id", req.CategoryId)
}
if req.Status != "" {
query = query.Where("status", req.Status)
}
var total int
rows := make([]*v1.HelpArticleItem, 0)
err := query.OrderAsc("sort_order").OrderDesc("created_at").
Page(page, pageSize).
ScanAndCount(&rows, &total, false)
if err != nil {
return nil, err
}
return &v1.HelpArticleListRes{
List: rows,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// GetHelpArticle 帮助文章详情(管理后台)
func (s *sAdmin) GetHelpArticle(ctx context.Context, req *v1.HelpArticleGetReq) (*v1.HelpArticleGetRes, error) {
var article *v1.HelpArticleGetRes
err := dao.SptArticles.Ctx(ctx).Where("id", req.Id).Scan(&article)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if article == nil {
return nil, common.NewBusinessError(consts.CodeHelpArticleNotFound, consts.MsgHelpArticleNotFound)
}
return article, nil
}
// refreshCategoryArticleCount 刷新分类的文章计数
func (s *sAdmin) refreshCategoryArticleCount(ctx context.Context, categoryId int64) {
count, err := dao.SptArticles.Ctx(ctx).
Where("category_id", categoryId).
Where("status", "published").
Where("published_at IS NOT NULL").
Where("published_at <=", gtime.Now()).
Count()
if err != nil {
g.Log().Warningf(ctx, "refreshCategoryArticleCount count failed for category %d: %v", categoryId, err)
return
}
_, err = dao.SptCategories.Ctx(ctx).
Where("id", categoryId).
Data(do.SptCategories{ArticleCount: count}).
Update()
if err != nil {
g.Log().Warningf(ctx, "refreshCategoryArticleCount update failed for category %d: %v", categoryId, err)
}
}