feat(admin): add comprehensive admin management system
- Add admin handlers and services for users, posts, comments, groups, and dashboard - Implement role permission management with ability to view and update permissions - Add database seeding for roles and Casbin permission policies - Upgrade Casbin from v2 to v3 with GORM adapter for persistent policy storage - Add admin-specific repository methods with filtering and pagination - Register new admin API routes under /api/v1/admin/*
This commit is contained in:
341
internal/service/admin_post_service.go
Normal file
341
internal/service/admin_post_service.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
"carrot_bbs/internal/dto"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
)
|
||||
|
||||
// AdminPostService 管理端帖子服务接口
|
||||
type AdminPostService interface {
|
||||
// GetPostList 获取帖子列表(分页、搜索、状态筛选)
|
||||
GetPostList(ctx context.Context, page, pageSize int, keyword, status, authorID, startDate, endDate string) ([]dto.AdminPostListResponse, int64, error)
|
||||
// GetPostDetail 获取帖子详情
|
||||
GetPostDetail(ctx context.Context, postID string) (*dto.AdminPostDetailResponse, error)
|
||||
// DeletePost 删除帖子
|
||||
DeletePost(ctx context.Context, postID string) error
|
||||
// ModeratePost 审核帖子
|
||||
ModeratePost(ctx context.Context, postID string, status model.PostStatus, reason, reviewedBy string) (*dto.AdminPostDetailResponse, error)
|
||||
// BatchDeletePosts 批量删除帖子
|
||||
BatchDeletePosts(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error)
|
||||
// BatchModeratePosts 批量审核帖子
|
||||
BatchModeratePosts(ctx context.Context, ids []string, status model.PostStatus, reviewedBy string) (*dto.AdminBatchOperationResponse, error)
|
||||
// SetPostPin 置顶/取消置顶帖子
|
||||
SetPostPin(ctx context.Context, postID string, isPinned bool) (*dto.AdminPostDetailResponse, error)
|
||||
// SetPostFeature 加精/取消加精帖子
|
||||
SetPostFeature(ctx context.Context, postID string, isFeatured bool) (*dto.AdminPostDetailResponse, error)
|
||||
}
|
||||
|
||||
// adminPostServiceImpl 管理端帖子服务实现
|
||||
type adminPostServiceImpl struct {
|
||||
postRepo *repository.PostRepository
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
// NewAdminPostService 创建管理端帖子服务
|
||||
func NewAdminPostService(postRepo *repository.PostRepository, cacheBackend cache.Cache) AdminPostService {
|
||||
return &adminPostServiceImpl{
|
||||
postRepo: postRepo,
|
||||
cache: cacheBackend,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPostList 获取帖子列表
|
||||
func (s *adminPostServiceImpl) GetPostList(ctx context.Context, page, pageSize int, keyword, status, authorID, startDate, endDate string) ([]dto.AdminPostListResponse, int64, error) {
|
||||
query := repository.AdminPostListQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Keyword: keyword,
|
||||
Status: status,
|
||||
AuthorID: authorID,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
}
|
||||
|
||||
posts, total, err := s.postRepo.AdminList(query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]dto.AdminPostListResponse, len(posts))
|
||||
for i, post := range posts {
|
||||
responses[i] = convertPostToAdminListResponse(post)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
// GetPostDetail 获取帖子详情
|
||||
func (s *adminPostServiceImpl) GetPostDetail(ctx context.Context, postID string) (*dto.AdminPostDetailResponse, error) {
|
||||
post, err := s.postRepo.GetByIDForAdmin(postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertPostToAdminDetailResponse(post), nil
|
||||
}
|
||||
|
||||
// DeletePost 删除帖子
|
||||
func (s *adminPostServiceImpl) DeletePost(ctx context.Context, postID string) error {
|
||||
err := s.postRepo.Delete(postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 失效缓存
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModeratePost 审核帖子
|
||||
func (s *adminPostServiceImpl) ModeratePost(ctx context.Context, postID string, status model.PostStatus, reason, reviewedBy string) (*dto.AdminPostDetailResponse, error) {
|
||||
// 先检查帖子是否存在
|
||||
_, err := s.postRepo.GetByIDForAdmin(postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新审核状态
|
||||
if err := s.postRepo.UpdateModerationStatus(postID, status, reason, reviewedBy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 失效缓存
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
// 重新获取帖子信息
|
||||
post, err := s.postRepo.GetByIDForAdmin(postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertPostToAdminDetailResponse(post), nil
|
||||
}
|
||||
|
||||
// BatchDeletePosts 批量删除帖子
|
||||
func (s *adminPostServiceImpl) BatchDeletePosts(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error) {
|
||||
failedIDs, err := s.postRepo.BatchDelete(ids)
|
||||
if err != nil {
|
||||
return &dto.AdminBatchOperationResponse{
|
||||
Success: false,
|
||||
Message: "批量删除失败",
|
||||
Failed: ids,
|
||||
}, err
|
||||
}
|
||||
|
||||
// 失效缓存
|
||||
for _, id := range ids {
|
||||
cache.InvalidatePostDetail(s.cache, id)
|
||||
}
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
if len(failedIDs) == 0 {
|
||||
return &dto.AdminBatchOperationResponse{
|
||||
Success: true,
|
||||
Message: "批量删除成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &dto.AdminBatchOperationResponse{
|
||||
Success: true,
|
||||
Message: "部分帖子删除成功",
|
||||
Failed: failedIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BatchModeratePosts 批量审核帖子
|
||||
func (s *adminPostServiceImpl) BatchModeratePosts(ctx context.Context, ids []string, status model.PostStatus, reviewedBy string) (*dto.AdminBatchOperationResponse, error) {
|
||||
failedIDs, err := s.postRepo.BatchUpdateStatus(ids, status, reviewedBy)
|
||||
if err != nil {
|
||||
return &dto.AdminBatchOperationResponse{
|
||||
Success: false,
|
||||
Message: "批量审核失败",
|
||||
Failed: ids,
|
||||
}, err
|
||||
}
|
||||
|
||||
// 失效缓存
|
||||
for _, id := range ids {
|
||||
cache.InvalidatePostDetail(s.cache, id)
|
||||
}
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
if len(failedIDs) == 0 {
|
||||
return &dto.AdminBatchOperationResponse{
|
||||
Success: true,
|
||||
Message: "批量审核成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &dto.AdminBatchOperationResponse{
|
||||
Success: true,
|
||||
Message: "部分帖子审核成功",
|
||||
Failed: failedIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetPostPin 置顶/取消置顶帖子
|
||||
func (s *adminPostServiceImpl) SetPostPin(ctx context.Context, postID string, isPinned bool) (*dto.AdminPostDetailResponse, error) {
|
||||
// 先检查帖子是否存在
|
||||
_, err := s.postRepo.GetByIDForAdmin(postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新置顶状态
|
||||
if err := s.postRepo.UpdatePinStatus(postID, isPinned); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 失效缓存
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
// 重新获取帖子信息
|
||||
post, err := s.postRepo.GetByIDForAdmin(postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertPostToAdminDetailResponse(post), nil
|
||||
}
|
||||
|
||||
// SetPostFeature 加精/取消加精帖子
|
||||
func (s *adminPostServiceImpl) SetPostFeature(ctx context.Context, postID string, isFeatured bool) (*dto.AdminPostDetailResponse, error) {
|
||||
// 先检查帖子是否存在
|
||||
_, err := s.postRepo.GetByIDForAdmin(postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新加精状态
|
||||
if err := s.postRepo.UpdateFeatureStatus(postID, isFeatured); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 失效缓存
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
cache.InvalidatePostList(s.cache)
|
||||
|
||||
// 重新获取帖子信息
|
||||
post, err := s.postRepo.GetByIDForAdmin(postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertPostToAdminDetailResponse(post), nil
|
||||
}
|
||||
|
||||
// convertPostToAdminListResponse 转换帖子为管理端列表响应
|
||||
func convertPostToAdminListResponse(post *model.Post) dto.AdminPostListResponse {
|
||||
images := make([]dto.PostImageResponse, len(post.Images))
|
||||
for i, img := range post.Images {
|
||||
images[i] = dto.PostImageResponse{
|
||||
ID: img.ID,
|
||||
URL: img.URL,
|
||||
ThumbnailURL: img.ThumbnailURL,
|
||||
Width: img.Width,
|
||||
Height: img.Height,
|
||||
}
|
||||
}
|
||||
|
||||
var author *dto.UserResponse
|
||||
if post.User != nil {
|
||||
author = &dto.UserResponse{
|
||||
ID: post.User.ID,
|
||||
Username: post.User.Username,
|
||||
Nickname: post.User.Nickname,
|
||||
Email: post.User.Email,
|
||||
Phone: post.User.Phone,
|
||||
Avatar: post.User.Avatar,
|
||||
Bio: post.User.Bio,
|
||||
Website: post.User.Website,
|
||||
Location: post.User.Location,
|
||||
CreatedAt: post.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
return dto.AdminPostListResponse{
|
||||
ID: post.ID,
|
||||
UserID: post.UserID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Images: images,
|
||||
Status: string(post.Status),
|
||||
LikesCount: post.LikesCount,
|
||||
CommentsCount: post.CommentsCount,
|
||||
FavoritesCount: post.FavoritesCount,
|
||||
ViewsCount: post.ViewsCount,
|
||||
IsPinned: post.IsPinned,
|
||||
IsFeatured: post.IsFeatured,
|
||||
IsLocked: post.IsLocked,
|
||||
RejectReason: post.RejectReason,
|
||||
ReviewedAt: dto.FormatTimePointer(post.ReviewedAt),
|
||||
ReviewedBy: post.ReviewedBy,
|
||||
CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
Author: author,
|
||||
}
|
||||
}
|
||||
|
||||
// convertPostToAdminDetailResponse 转换帖子为管理端详情响应
|
||||
func convertPostToAdminDetailResponse(post *model.Post) *dto.AdminPostDetailResponse {
|
||||
images := make([]dto.PostImageResponse, len(post.Images))
|
||||
for i, img := range post.Images {
|
||||
images[i] = dto.PostImageResponse{
|
||||
ID: img.ID,
|
||||
URL: img.URL,
|
||||
ThumbnailURL: img.ThumbnailURL,
|
||||
Width: img.Width,
|
||||
Height: img.Height,
|
||||
}
|
||||
}
|
||||
|
||||
var author *dto.UserResponse
|
||||
if post.User != nil {
|
||||
author = &dto.UserResponse{
|
||||
ID: post.User.ID,
|
||||
Username: post.User.Username,
|
||||
Nickname: post.User.Nickname,
|
||||
Email: post.User.Email,
|
||||
Phone: post.User.Phone,
|
||||
Avatar: post.User.Avatar,
|
||||
Bio: post.User.Bio,
|
||||
Website: post.User.Website,
|
||||
Location: post.User.Location,
|
||||
CreatedAt: post.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.AdminPostDetailResponse{
|
||||
ID: post.ID,
|
||||
UserID: post.UserID,
|
||||
CommunityID: post.CommunityID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Images: images,
|
||||
Status: string(post.Status),
|
||||
LikesCount: post.LikesCount,
|
||||
CommentsCount: post.CommentsCount,
|
||||
FavoritesCount: post.FavoritesCount,
|
||||
SharesCount: post.SharesCount,
|
||||
ViewsCount: post.ViewsCount,
|
||||
HotScore: post.HotScore,
|
||||
IsPinned: post.IsPinned,
|
||||
IsFeatured: post.IsFeatured,
|
||||
IsLocked: post.IsLocked,
|
||||
IsVote: post.IsVote,
|
||||
RejectReason: post.RejectReason,
|
||||
ReviewedAt: dto.FormatTimePointer(post.ReviewedAt),
|
||||
ReviewedBy: post.ReviewedBy,
|
||||
CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
Author: author,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user