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:
2026-03-14 18:01:55 +08:00
parent ae5b997924
commit d3065b30cc
28 changed files with 4018 additions and 107 deletions

View File

@@ -0,0 +1,243 @@
package service
import (
"context"
"encoding/json"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// AdminCommentService 管理端评论服务接口
type AdminCommentService interface {
// GetCommentList 获取评论列表(分页、搜索、状态筛选)
GetCommentList(ctx context.Context, page, pageSize int, keyword, postID, authorID, status, startDate, endDate string) ([]dto.AdminCommentListResponse, int64, error)
// GetCommentDetail 获取评论详情
GetCommentDetail(ctx context.Context, commentID string) (*dto.AdminCommentDetailResponse, error)
// DeleteComment 删除评论
DeleteComment(ctx context.Context, commentID string) error
// BatchDeleteComments 批量删除评论
BatchDeleteComments(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error)
// GetCommentsByPostID 获取帖子的评论列表
GetCommentsByPostID(ctx context.Context, postID string, page, pageSize int) ([]dto.AdminCommentListResponse, int64, error)
}
// adminCommentServiceImpl 管理端评论服务实现
type adminCommentServiceImpl struct {
commentRepo *repository.CommentRepository
postRepo *repository.PostRepository
}
// NewAdminCommentService 创建管理端评论服务
func NewAdminCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository) AdminCommentService {
return &adminCommentServiceImpl{
commentRepo: commentRepo,
postRepo: postRepo,
}
}
// GetCommentList 获取评论列表
func (s *adminCommentServiceImpl) GetCommentList(ctx context.Context, page, pageSize int, keyword, postID, authorID, status, startDate, endDate string) ([]dto.AdminCommentListResponse, int64, error) {
filter := repository.AdminCommentListFilter{
Keyword: keyword,
PostID: postID,
AuthorID: authorID,
Status: status,
StartDate: startDate,
EndDate: endDate,
Page: page,
PageSize: pageSize,
}
comments, total, err := s.commentRepo.GetAdminCommentList(filter)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminCommentListResponse, len(comments))
for i, comment := range comments {
responses[i] = s.convertCommentToAdminListResponse(comment)
}
return responses, total, nil
}
// GetCommentDetail 获取评论详情
func (s *adminCommentServiceImpl) GetCommentDetail(ctx context.Context, commentID string) (*dto.AdminCommentDetailResponse, error) {
comment, err := s.commentRepo.GetAdminCommentByID(commentID)
if err != nil {
return nil, err
}
return s.convertCommentToAdminDetailResponse(comment), nil
}
// DeleteComment 删除评论
func (s *adminCommentServiceImpl) DeleteComment(ctx context.Context, commentID string) error {
return s.commentRepo.Delete(commentID)
}
// BatchDeleteComments 批量删除评论
func (s *adminCommentServiceImpl) BatchDeleteComments(ctx context.Context, ids []string) (*dto.AdminBatchOperationResponse, error) {
failedIDs, err := s.commentRepo.BatchDelete(ids)
if err != nil {
return &dto.AdminBatchOperationResponse{
Success: false,
Message: "批量删除失败",
Failed: ids,
}, err
}
if len(failedIDs) == 0 {
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "批量删除成功",
}, nil
}
return &dto.AdminBatchOperationResponse{
Success: true,
Message: "部分评论删除成功",
Failed: failedIDs,
}, nil
}
// GetCommentsByPostID 获取帖子的评论列表
func (s *adminCommentServiceImpl) GetCommentsByPostID(ctx context.Context, postID string, page, pageSize int) ([]dto.AdminCommentListResponse, int64, error) {
comments, total, err := s.commentRepo.GetCommentsByPostIDForAdmin(postID, page, pageSize)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminCommentListResponse, len(comments))
for i, comment := range comments {
responses[i] = s.convertCommentToAdminListResponse(comment)
}
return responses, total, nil
}
// convertCommentToAdminListResponse 转换评论为管理端列表响应
func (s *adminCommentServiceImpl) convertCommentToAdminListResponse(comment *model.Comment) dto.AdminCommentListResponse {
// 解析图片
var images []dto.CommentImageResponse
if comment.Images != "" {
var urls []string
if err := json.Unmarshal([]byte(comment.Images), &urls); err == nil {
images = make([]dto.CommentImageResponse, len(urls))
for i, url := range urls {
images[i] = dto.CommentImageResponse{URL: url}
}
}
}
// 作者信息
var author *dto.UserResponse
if comment.User != nil {
author = &dto.UserResponse{
ID: comment.User.ID,
Username: comment.User.Username,
Nickname: comment.User.Nickname,
Email: comment.User.Email,
Phone: comment.User.Phone,
Avatar: comment.User.Avatar,
Bio: comment.User.Bio,
Website: comment.User.Website,
Location: comment.User.Location,
CreatedAt: comment.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
// 帖子简要信息
var postInfo *dto.AdminCommentPostInfo
if comment.PostID != "" {
postInfo = &dto.AdminCommentPostInfo{
ID: comment.PostID,
}
// 尝试获取帖子标题
post, err := s.postRepo.GetByID(comment.PostID)
if err == nil && post != nil {
postInfo.Title = post.Title
}
}
return dto.AdminCommentListResponse{
ID: comment.ID,
PostID: comment.PostID,
UserID: comment.UserID,
ParentID: comment.ParentID,
RootID: comment.RootID,
Content: comment.Content,
Images: images,
Status: string(comment.Status),
LikesCount: comment.LikesCount,
RepliesCount: comment.RepliesCount,
CreatedAt: comment.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: comment.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Author: author,
Post: postInfo,
}
}
// convertCommentToAdminDetailResponse 转换评论为管理端详情响应
func (s *adminCommentServiceImpl) convertCommentToAdminDetailResponse(comment *model.Comment) *dto.AdminCommentDetailResponse {
// 解析图片
var images []dto.CommentImageResponse
if comment.Images != "" {
var urls []string
if err := json.Unmarshal([]byte(comment.Images), &urls); err == nil {
images = make([]dto.CommentImageResponse, len(urls))
for i, url := range urls {
images[i] = dto.CommentImageResponse{URL: url}
}
}
}
// 作者信息
var author *dto.UserResponse
if comment.User != nil {
author = &dto.UserResponse{
ID: comment.User.ID,
Username: comment.User.Username,
Nickname: comment.User.Nickname,
Email: comment.User.Email,
Phone: comment.User.Phone,
Avatar: comment.User.Avatar,
Bio: comment.User.Bio,
Website: comment.User.Website,
Location: comment.User.Location,
CreatedAt: comment.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
// 帖子简要信息
var postInfo *dto.AdminCommentPostInfo
if comment.PostID != "" {
postInfo = &dto.AdminCommentPostInfo{
ID: comment.PostID,
}
// 尝试获取帖子标题
post, err := s.postRepo.GetByID(comment.PostID)
if err == nil && post != nil {
postInfo.Title = post.Title
}
}
return &dto.AdminCommentDetailResponse{
ID: comment.ID,
PostID: comment.PostID,
UserID: comment.UserID,
ParentID: comment.ParentID,
RootID: comment.RootID,
Content: comment.Content,
Images: images,
Status: string(comment.Status),
LikesCount: comment.LikesCount,
RepliesCount: comment.RepliesCount,
CreatedAt: comment.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: comment.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Author: author,
Post: postInfo,
}
}

View File

@@ -0,0 +1,444 @@
package service
import (
"context"
"time"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"gorm.io/gorm"
)
// AdminDashboardService 管理端仪表盘服务接口
type AdminDashboardService interface {
// GetStats 获取统计数据
GetStats(ctx context.Context) (*dto.DashboardStatsResponse, error)
// GetUserActivityTrend 获取用户活跃度趋势
GetUserActivityTrend(ctx context.Context, days int) ([]dto.UserActivityTrendResponse, error)
// GetContentStats 获取内容统计
GetContentStats(ctx context.Context) (*dto.ContentStatsResponse, error)
// GetPendingContent 获取待审核内容
GetPendingContent(ctx context.Context, limit int) ([]dto.PendingContentResponse, error)
// GetOnlineUsers 获取在线用户数
GetOnlineUsers(ctx context.Context) (*dto.OnlineUsersResponse, error)
}
// adminDashboardServiceImpl 管理端仪表盘服务实现
type adminDashboardServiceImpl struct {
db *gorm.DB
userRepo *repository.UserRepository
postRepo *repository.PostRepository
commentRepo *repository.CommentRepository
groupRepo repository.GroupRepository
activityRepo *repository.UserActivityRepository
}
// NewAdminDashboardService 创建管理端仪表盘服务
func NewAdminDashboardService(
db *gorm.DB,
userRepo *repository.UserRepository,
postRepo *repository.PostRepository,
commentRepo *repository.CommentRepository,
groupRepo repository.GroupRepository,
activityRepo *repository.UserActivityRepository,
) AdminDashboardService {
return &adminDashboardServiceImpl{
db: db,
userRepo: userRepo,
postRepo: postRepo,
commentRepo: commentRepo,
groupRepo: groupRepo,
activityRepo: activityRepo,
}
}
// GetStats 获取统计数据
func (s *adminDashboardServiceImpl) GetStats(ctx context.Context) (*dto.DashboardStatsResponse, error) {
var stats dto.DashboardStatsResponse
// 获取当前时间
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
weekStart := todayStart.AddDate(0, 0, -int(now.Weekday()))
// 总用户数
if err := s.db.Model(&model.User{}).Count(&stats.TotalUsers).Error; err != nil {
return nil, err
}
// 今日新增用户
if err := s.db.Model(&model.User{}).Where("created_at >= ?", todayStart).Count(&stats.TodayNewUsers).Error; err != nil {
return nil, err
}
// 今日活跃用户
todayActive, err := s.activityRepo.GetDAU(ctx, now)
if err != nil {
// 如果出错,尝试从数据库获取
stats.ActiveUsersToday = 0
} else {
stats.ActiveUsersToday = todayActive
}
// 本周活跃用户(使用 WAU
year, week := now.ISOWeek()
weekActive, err := s.activityRepo.GetWAUFromRedis(ctx, year, week)
if err != nil {
// 如果 Redis 获取失败,从数据库计算
var weekActiveCount int64
err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", weekStart).
Distinct("user_id").
Count(&weekActiveCount).Error
if err == nil {
stats.ActiveUsersWeek = weekActiveCount
}
} else {
stats.ActiveUsersWeek = weekActive
}
// 本月活跃用户(使用 MAU
monthActive, err := s.activityRepo.GetMAUFromRedis(ctx, now.Year(), int(now.Month()))
if err != nil {
// 如果 Redis 获取失败,从数据库计算
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
var monthActiveCount int64
err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", monthStart).
Distinct("user_id").
Count(&monthActiveCount).Error
if err == nil {
stats.ActiveUsersMonth = monthActiveCount
}
} else {
stats.ActiveUsersMonth = monthActive
}
// 获取留存率数据(从统计快照表获取)
retentionStats := s.getRetentionStats(ctx, now)
stats.Retention1Day = retentionStats.Retention1Day
stats.Retention7Day = retentionStats.Retention7Day
stats.Retention30Day = retentionStats.Retention30Day
// 总帖子数
if err := s.db.Model(&model.Post{}).Count(&stats.TotalPosts).Error; err != nil {
return nil, err
}
// 今日帖子数
if err := s.db.Model(&model.Post{}).Where("created_at >= ?", todayStart).Count(&stats.TodayPosts).Error; err != nil {
return nil, err
}
// 待审核帖子数
if err := s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPending).Count(&stats.PendingPosts).Error; err != nil {
return nil, err
}
// 总评论数
if err := s.db.Model(&model.Comment{}).Count(&stats.TotalComments).Error; err != nil {
return nil, err
}
// 今日评论数
if err := s.db.Model(&model.Comment{}).Where("created_at >= ?", todayStart).Count(&stats.TodayComments).Error; err != nil {
return nil, err
}
// 总群组数
if err := s.db.Model(&model.Group{}).Count(&stats.TotalGroups).Error; err != nil {
return nil, err
}
// 今日新建群组
if err := s.db.Model(&model.Group{}).Where("created_at >= ?", todayStart).Count(&stats.TodayNewGroups).Error; err != nil {
return nil, err
}
// 待审核评论数
var pendingComments int64
if err := s.db.Model(&model.Comment{}).Where("status = ?", model.CommentStatusPending).Count(&pendingComments).Error; err != nil {
return nil, err
}
stats.PendingComments = pendingComments
// 计算待审核总数
stats.PendingReview = stats.PendingPosts + stats.PendingComments
stats.PendingPostsCount = stats.PendingPosts
return &stats, nil
}
// GetUserActivityTrend 获取用户活跃度趋势
func (s *adminDashboardServiceImpl) GetUserActivityTrend(ctx context.Context, days int) ([]dto.UserActivityTrendResponse, error) {
if days <= 0 {
days = 7
}
if days > 30 {
days = 30
}
now := time.Now()
results := make([]dto.UserActivityTrendResponse, days)
for i := days - 1; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
dateEnd := dateStart.Add(24 * time.Hour)
trend := dto.UserActivityTrendResponse{
Date: dateStart.Format("2006-01-02"),
}
// 新增用户数
var newUsers int64
if err := s.db.Model(&model.User{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&newUsers).Error; err == nil {
trend.NewUsers = newUsers
}
// 活跃用户数
activeUsers, err := s.activityRepo.GetDAU(ctx, date)
if err == nil {
trend.ActiveUsers = activeUsers
}
// 帖子数
var posts int64
if err := s.db.Model(&model.Post{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&posts).Error; err == nil {
trend.Posts = posts
}
// 评论数
var comments int64
if err := s.db.Model(&model.Comment{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&comments).Error; err == nil {
trend.Comments = comments
}
results[days-1-i] = trend
}
return results, nil
}
// GetContentStats 获取内容统计
func (s *adminDashboardServiceImpl) GetContentStats(ctx context.Context) (*dto.ContentStatsResponse, error) {
var response dto.ContentStatsResponse
// 内容分布
var totalPosts, totalComments, totalGroups int64
s.db.Model(&model.Post{}).Count(&totalPosts)
s.db.Model(&model.Comment{}).Count(&totalComments)
s.db.Model(&model.Group{}).Count(&totalGroups)
response.ContentDistribution = []dto.ContentDistributionItem{
{Type: "posts", Name: "帖子", Count: totalPosts},
{Type: "comments", Name: "评论", Count: totalComments},
{Type: "groups", Name: "群组", Count: totalGroups},
}
// 帖子状态分布
var publishedCount, pendingCount, rejectedCount, draftCount int64
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).Count(&publishedCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPending).Count(&pendingCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusRejected).Count(&rejectedCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusDraft).Count(&draftCount)
response.PostStatusDistribution = []dto.PostStatusDistributionItem{
{Status: string(model.PostStatusPublished), Name: "已发布", Count: publishedCount},
{Status: string(model.PostStatusPending), Name: "待审核", Count: pendingCount},
{Status: string(model.PostStatusRejected), Name: "已拒绝", Count: rejectedCount},
{Status: string(model.PostStatusDraft), Name: "草稿", Count: draftCount},
}
// 用户增长趋势最近7天
now := time.Now()
response.UserGrowth = make([]dto.UserGrowthItem, 7)
for i := 6; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
dateEnd := dateStart.Add(24 * time.Hour)
var newUsers int64
s.db.Model(&model.User{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&newUsers)
response.UserGrowth[6-i] = dto.UserGrowthItem{
Date: dateStart.Format("2006-01-02"),
NewUsers: newUsers,
}
}
return &response, nil
}
// GetPendingContent 获取待审核内容
func (s *adminDashboardServiceImpl) GetPendingContent(ctx context.Context, limit int) ([]dto.PendingContentResponse, error) {
if limit <= 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
results := make([]dto.PendingContentResponse, 0)
// 获取待审核帖子
var posts []*model.Post
if err := s.db.Preload("User").
Where("status = ?", model.PostStatusPending).
Order("created_at DESC").
Limit(limit).
Find(&posts).Error; err != nil {
return nil, err
}
for _, post := range posts {
item := dto.PendingContentResponse{
ID: post.ID,
Type: "post",
Title: post.Title,
Content: post.Content,
CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if post.User != nil {
item.Author = &dto.PendingContentAuthor{
ID: post.User.ID,
Username: post.User.Username,
Nickname: post.User.Nickname,
Avatar: post.User.Avatar,
}
}
results = append(results, item)
}
// 获取待审核评论
var comments []*model.Comment
if err := s.db.Preload("User").
Where("status = ?", model.CommentStatusPending).
Order("created_at DESC").
Limit(limit).
Find(&comments).Error; err != nil {
return nil, err
}
for _, comment := range comments {
item := dto.PendingContentResponse{
ID: comment.ID,
Type: "comment",
Title: "",
Content: comment.Content,
CreatedAt: comment.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if comment.User != nil {
item.Author = &dto.PendingContentAuthor{
ID: comment.User.ID,
Username: comment.User.Username,
Nickname: comment.User.Nickname,
Avatar: comment.User.Avatar,
}
}
results = append(results, item)
}
// 按时间排序并限制数量
if len(results) > limit {
results = results[:limit]
}
return results, nil
}
// GetOnlineUsers 获取在线用户数
func (s *adminDashboardServiceImpl) GetOnlineUsers(ctx context.Context) (*dto.OnlineUsersResponse, error) {
// 获取当前日期的活跃用户数作为在线用户数的近似值
// 实际应用中可能需要使用 WebSocket 连接数或 Redis 中的在线状态
now := time.Now()
count, err := s.activityRepo.GetDAU(ctx, now)
if err != nil {
// 如果 Redis 获取失败,从数据库获取
var dbCount int64
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
if err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", todayStart).
Distinct("user_id").
Count(&dbCount).Error; err != nil {
return nil, err
}
count = dbCount
}
return &dto.OnlineUsersResponse{
Count: count,
}, nil
}
// retentionStatsResult 留存率结果
type retentionStatsResult struct {
Retention1Day int64
Retention7Day int64
Retention30Day int64
}
// getRetentionStats 获取留存率统计数据
func (s *adminDashboardServiceImpl) getRetentionStats(ctx context.Context, now time.Time) retentionStatsResult {
var result retentionStatsResult
// 尝试从统计快照表获取昨日的留存率数据
yesterday := now.AddDate(0, 0, -1)
if stat, err := s.activityRepo.GetStat(ctx, "dau", yesterday); err == nil {
result.Retention1Day = int64(stat.Retention1)
result.Retention7Day = int64(stat.Retention7)
result.Retention30Day = int64(stat.Retention30)
return result
}
// 如果快照表没有数据,实时计算留存率
// 次日留存率:昨日新增用户中今日仍活跃的比例
result.Retention1Day = s.calculateRetention(ctx, 1)
result.Retention7Day = s.calculateRetention(ctx, 7)
result.Retention30Day = s.calculateRetention(ctx, 30)
return result
}
// calculateRetention 计算指定天数的留存率
func (s *adminDashboardServiceImpl) calculateRetention(ctx context.Context, days int) int64 {
now := time.Now()
// 获取days天前的新增用户
targetDate := now.AddDate(0, 0, -days)
targetDateStart := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), 0, 0, 0, 0, targetDate.Location())
targetDateEnd := targetDateStart.Add(24 * time.Hour)
var newUsers []string
if err := s.db.Model(&model.User{}).
Select("id").
Where("created_at >= ? AND created_at < ?", targetDateStart, targetDateEnd).
Pluck("id", &newUsers).Error; err != nil || len(newUsers) == 0 {
return 0
}
// 检查这些用户在今日是否活跃
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
var activeCount int64
if err := s.db.Model(&model.UserActiveLog{}).
Where("user_id IN ?", newUsers).
Where("active_date >= ?", todayStart).
Distinct("user_id").
Count(&activeCount).Error; err != nil {
return 0
}
// 计算留存率(百分比)
retention := float64(activeCount) / float64(len(newUsers)) * 100
return int64(retention)
}

View File

@@ -0,0 +1,390 @@
package service
import (
"context"
"errors"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// AdminGroupService 管理端群组服务接口
type AdminGroupService interface {
// GetGroupList 获取群组列表(分页、搜索、筛选)
GetGroupList(ctx context.Context, page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]dto.AdminGroupListResponse, int64, error)
// GetGroupDetail 获取群组详情
GetGroupDetail(ctx context.Context, groupID string) (*dto.AdminGroupDetailResponse, error)
// GetGroupMembers 获取群组成员列表
GetGroupMembers(ctx context.Context, groupID string, page, pageSize int) (*dto.AdminGroupMemberListResponse, error)
// DeleteGroup 解散群组
DeleteGroup(ctx context.Context, groupID string) error
// UpdateGroupStatus 更新群组状态
UpdateGroupStatus(ctx context.Context, groupID string, status string) (*dto.AdminGroupDetailResponse, error)
// RemoveMember 移除群组成员
RemoveMember(ctx context.Context, groupID string, userID string) error
// TransferOwner 转让群主
TransferOwner(ctx context.Context, groupID string, newOwnerID string) (*dto.AdminGroupDetailResponse, error)
}
// adminGroupServiceImpl 管理端群组服务实现
type adminGroupServiceImpl struct {
groupRepo repository.GroupRepository
userRepo *repository.UserRepository
}
// NewAdminGroupService 创建管理端群组服务
func NewAdminGroupService(
groupRepo repository.GroupRepository,
userRepo *repository.UserRepository,
) AdminGroupService {
return &adminGroupServiceImpl{
groupRepo: groupRepo,
userRepo: userRepo,
}
}
// GetGroupList 获取群组列表
func (s *adminGroupServiceImpl) GetGroupList(ctx context.Context, page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]dto.AdminGroupListResponse, int64, error) {
groups, total, err := s.groupRepo.GetGroupList(page, pageSize, keyword, status, ownerID, startDate, endDate)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminGroupListResponse, 0, len(groups))
for _, group := range groups {
response := dto.AdminGroupListResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: "active", // 默认状态为active后续可以根据需要添加状态字段
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Avatar: owner.Avatar,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
// 获取帖子数量
postCount, _ := s.groupRepo.GetGroupPostCount(group.ID)
response.PostCount = postCount
responses = append(responses, response)
}
return responses, total, nil
}
// GetGroupDetail 获取群组详情
func (s *adminGroupServiceImpl) GetGroupDetail(ctx context.Context, groupID string) (*dto.AdminGroupDetailResponse, error) {
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, err
}
response := &dto.AdminGroupDetailResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: "active",
IsPublic: true, // 默认为公开群组,后续可以根据需要添加字段
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Email: owner.Email,
Phone: owner.Phone,
Avatar: owner.Avatar,
Bio: owner.Bio,
Website: owner.Website,
Location: owner.Location,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
// 获取帖子数量
postCount, _ := s.groupRepo.GetGroupPostCount(group.ID)
response.PostCount = postCount
// 获取最新公告
announcement, _ := s.groupRepo.GetLatestAnnouncement(groupID)
if announcement != nil {
response.Announcement = &dto.GroupAnnouncementResponse{
ID: announcement.ID,
GroupID: announcement.GroupID,
Content: announcement.Content,
AuthorID: announcement.AuthorID,
IsPinned: announcement.IsPinned,
CreatedAt: dto.FormatTime(announcement.CreatedAt),
}
}
return response, nil
}
// GetGroupMembers 获取群组成员列表
func (s *adminGroupServiceImpl) GetGroupMembers(ctx context.Context, groupID string, page, pageSize int) (*dto.AdminGroupMemberListResponse, error) {
// 先检查群组是否存在
_, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, errors.New("群组不存在")
}
members, total, err := s.groupRepo.GetMembersWithUserInfo(groupID, page, pageSize)
if err != nil {
return nil, err
}
list := make([]*dto.AdminGroupMemberResponse, 0, len(members))
for _, member := range members {
item := &dto.AdminGroupMemberResponse{
ID: member.ID,
UserID: member.UserID,
Role: member.Role,
Nickname: member.Nickname,
Muted: member.Muted,
JoinedAt: dto.FormatTime(member.JoinTime),
}
if member.User != nil {
item.Username = member.User.Username
item.Nickname = member.User.Nickname
item.Avatar = member.User.Avatar
item.User = &dto.UserResponse{
ID: member.User.ID,
Username: member.User.Username,
Nickname: member.User.Nickname,
Avatar: member.User.Avatar,
CreatedAt: dto.FormatTime(member.User.CreatedAt),
}
}
list = append(list, item)
}
return &dto.AdminGroupMemberListResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// DeleteGroup 解散群组
func (s *adminGroupServiceImpl) DeleteGroup(ctx context.Context, groupID string) error {
// 先检查群组是否存在
_, err := s.groupRepo.GetByID(groupID)
if err != nil {
return errors.New("群组不存在")
}
return s.groupRepo.Delete(groupID)
}
// UpdateGroupStatus 更新群组状态
func (s *adminGroupServiceImpl) UpdateGroupStatus(ctx context.Context, groupID string, status string) (*dto.AdminGroupDetailResponse, error) {
// 先检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, errors.New("群组不存在")
}
// 验证状态值
if status != "active" && status != "dissolved" {
return nil, errors.New("无效的状态值")
}
// 如果状态是dissolved则解散群组
if status == "dissolved" {
err = s.groupRepo.Delete(groupID)
if err != nil {
return nil, err
}
} else {
// 更新群组状态
err = s.groupRepo.UpdateGroupStatus(groupID, status)
if err != nil {
return nil, err
}
// 重新获取群组信息
group, err = s.groupRepo.GetByID(groupID)
if err != nil {
return nil, err
}
}
// 构建响应
response := &dto.AdminGroupDetailResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: status,
IsPublic: true,
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Avatar: owner.Avatar,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
return response, nil
}
// RemoveMember 移除群组成员
func (s *adminGroupServiceImpl) RemoveMember(ctx context.Context, groupID string, userID string) error {
// 先检查群组是否存在
_, err := s.groupRepo.GetByID(groupID)
if err != nil {
return errors.New("群组不存在")
}
// 检查用户是否是群成员
member, err := s.groupRepo.GetMember(groupID, userID)
if err != nil {
return errors.New("用户不是群组成员")
}
// 不能移除群主
if member.Role == model.GroupRoleOwner {
return errors.New("不能移除群主")
}
return s.groupRepo.RemoveMember(groupID, userID)
}
// TransferOwner 转让群主
func (s *adminGroupServiceImpl) TransferOwner(ctx context.Context, groupID string, newOwnerID string) (*dto.AdminGroupDetailResponse, error) {
// 先检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return nil, errors.New("群组不存在")
}
// 检查新群主是否是群成员
newOwnerMember, err := s.groupRepo.GetMember(groupID, newOwnerID)
if err != nil {
return nil, errors.New("新群主不是群组成员")
}
// 获取当前群主信息
currentOwnerMember, err := s.groupRepo.GetMember(groupID, group.OwnerID)
if err != nil {
return nil, errors.New("当前群主信息不存在")
}
oldOwnerID := group.OwnerID
// 更新当前群主为普通成员
err = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleMember)
if err != nil {
return nil, err
}
// 更新新群主角色
err = s.groupRepo.SetMemberRole(groupID, newOwnerID, model.GroupRoleOwner)
if err != nil {
// 回滚
_ = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleOwner)
return nil, err
}
// 更新群组的OwnerID
group.OwnerID = newOwnerID
err = s.groupRepo.Update(group)
if err != nil {
// 回滚
_ = s.groupRepo.SetMemberRole(groupID, oldOwnerID, model.GroupRoleOwner)
_ = s.groupRepo.SetMemberRole(groupID, newOwnerID, newOwnerMember.Role)
return nil, err
}
// 重新获取群组信息
group, err = s.groupRepo.GetByID(groupID)
if err != nil {
return nil, err
}
// 构建响应
response := &dto.AdminGroupDetailResponse{
ID: group.ID,
Name: group.Name,
Avatar: group.Avatar,
Description: group.Description,
OwnerID: group.OwnerID,
MemberCount: group.MemberCount,
MaxMembers: group.MaxMembers,
JoinType: int(group.JoinType),
MuteAll: group.MuteAll,
Status: "active",
IsPublic: true,
CreatedAt: dto.FormatTime(group.CreatedAt),
UpdatedAt: dto.FormatTime(group.UpdatedAt),
}
// 获取新群主信息
if group.OwnerID != "" {
owner, err := s.userRepo.GetByID(group.OwnerID)
if err == nil && owner != nil {
response.Owner = &dto.UserResponse{
ID: owner.ID,
Username: owner.Username,
Nickname: owner.Nickname,
Avatar: owner.Avatar,
CreatedAt: dto.FormatTime(owner.CreatedAt),
}
}
}
_ = currentOwnerMember // 避免 unused variable 警告
return response, nil
}

View 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,
}
}

View File

@@ -0,0 +1,145 @@
package service
import (
"context"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
)
// AdminUserService 管理端用户服务接口
type AdminUserService interface {
// GetUserList 获取用户列表(分页、搜索、状态筛选)
GetUserList(ctx context.Context, page, pageSize int, keyword, status string) ([]dto.AdminUserListResponse, int64, error)
// GetUserDetail 获取用户详情(含统计信息)
GetUserDetail(ctx context.Context, userID string) (*dto.AdminUserDetailResponse, error)
// UpdateUserStatus 更新用户状态
UpdateUserStatus(ctx context.Context, userID string, status model.UserStatus) (*dto.AdminUserDetailResponse, error)
}
// adminUserServiceImpl 管理端用户服务实现
type adminUserServiceImpl struct {
userRepo *repository.UserRepository
}
// NewAdminUserService 创建管理端用户服务
func NewAdminUserService(userRepo *repository.UserRepository) AdminUserService {
return &adminUserServiceImpl{
userRepo: userRepo,
}
}
// GetUserList 获取用户列表
func (s *adminUserServiceImpl) GetUserList(ctx context.Context, page, pageSize int, keyword, status string) ([]dto.AdminUserListResponse, int64, error) {
users, total, err := s.userRepo.AdminList(page, pageSize, keyword, status)
if err != nil {
return nil, 0, err
}
responses := make([]dto.AdminUserListResponse, len(users))
for i, user := range users {
responses[i] = convertUserToAdminListResponse(user)
}
return responses, total, nil
}
// GetUserDetail 获取用户详情
func (s *adminUserServiceImpl) GetUserDetail(ctx context.Context, userID string) (*dto.AdminUserDetailResponse, error) {
user, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
// 获取帖子数
postsCount, _ := s.userRepo.GetPostsCount(userID)
// 获取评论数
commentsCount, _ := s.userRepo.GetCommentsCount(userID)
return convertUserToAdminDetailResponse(user, postsCount, commentsCount), nil
}
// UpdateUserStatus 更新用户状态
func (s *adminUserServiceImpl) UpdateUserStatus(ctx context.Context, userID string, status model.UserStatus) (*dto.AdminUserDetailResponse, error) {
// 先检查用户是否存在
user, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
// 更新状态
if err := s.userRepo.UpdateStatus(userID, status); err != nil {
return nil, err
}
// 重新获取用户信息
user, err = s.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
// 获取统计信息
postsCount, _ := s.userRepo.GetPostsCount(userID)
commentsCount, _ := s.userRepo.GetCommentsCount(userID)
return convertUserToAdminDetailResponse(user, postsCount, commentsCount), nil
}
// convertUserToAdminListResponse 转换用户为管理端列表响应
func convertUserToAdminListResponse(user *model.User) dto.AdminUserListResponse {
var lastLoginAt string
if user.LastLoginAt != nil {
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
}
return dto.AdminUserListResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: user.Avatar,
Status: string(user.Status),
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
LastLoginAt: lastLoginAt,
LastLoginIP: user.LastLoginIP,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
// convertUserToAdminDetailResponse 转换用户为管理端详情响应
func convertUserToAdminDetailResponse(user *model.User, postsCount, commentsCount int64) *dto.AdminUserDetailResponse {
var lastLoginAt string
if user.LastLoginAt != nil {
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
}
return &dto.AdminUserDetailResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: user.Avatar,
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
IsVerified: user.IsVerified,
Status: string(user.Status),
PostsCount: int(postsCount),
CommentsCount: commentsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
LastLoginAt: lastLoginAt,
LastLoginIP: user.LastLoginIP,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}

View File

@@ -9,7 +9,7 @@ import (
"carrot_bbs/internal/config"
"carrot_bbs/internal/repository"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v3"
)
// CasbinService Casbin 权限服务接口
@@ -43,6 +43,15 @@ type CasbinService interface {
// ClearCache 清除权限缓存
ClearCache(ctx context.Context) error
// GetPermissionsForRole 获取角色的所有权限
GetPermissionsForRole(ctx context.Context, role string) ([][]string, error)
// UpdatePermissionsForRole 更新角色的权限(先删除所有权限,再添加新权限)
UpdatePermissionsForRole(ctx context.Context, role string, permissions [][]string) error
// GetAllPermissions 获取系统中所有权限定义
GetAllPermissions(ctx context.Context) [][]string
}
type casbinService struct {
@@ -223,3 +232,34 @@ func (s *casbinService) clearUserCache(ctx context.Context, userID string) error
}
return nil
}
func (s *casbinService) GetPermissionsForRole(ctx context.Context, role string) ([][]string, error) {
return s.enforcer.GetPermissionsForUser(role)
}
func (s *casbinService) UpdatePermissionsForRole(ctx context.Context, role string, permissions [][]string) error {
// 先删除该角色的所有权限
_, err := s.enforcer.DeletePermissionsForUser(role)
if err != nil {
return err
}
// 添加新权限
for _, perm := range permissions {
if len(perm) >= 2 {
resource := perm[0]
action := perm[1]
if _, err := s.enforcer.AddPolicy(role, resource, action); err != nil {
return err
}
}
}
// 清除缓存
return s.ClearCache(ctx)
}
func (s *casbinService) GetAllPermissions(ctx context.Context) [][]string {
permissions, _ := s.enforcer.GetPolicy()
return permissions
}

View File

@@ -24,6 +24,37 @@ type RoleService interface {
// GetRole 获取角色详情
GetRole(ctx context.Context, name string) (*model.Role, error)
// GetRoleDetail 获取角色详情(包含用户数量和权限)
GetRoleDetail(ctx context.Context, name string) (*RoleDetail, error)
// GetRolePermissions 获取角色的权限列表
GetRolePermissions(ctx context.Context, name string) ([]Permission, error)
// GetAllPermissions 获取系统中所有权限定义
GetAllPermissions(ctx context.Context) ([]Permission, error)
// UpdateRolePermissions 更新角色权限
UpdateRolePermissions(ctx context.Context, roleName string, permissions []Permission) error
}
// Permission 权限定义
type Permission struct {
Resource string `json:"resource"`
Action string `json:"action"`
}
// RoleDetail 角色详情
type RoleDetail struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
ParentRole *string `json:"parent_role"`
Priority int `json:"priority"`
UserCount int64 `json:"user_count"`
Permissions []Permission `json:"permissions"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type roleService struct {
@@ -114,3 +145,106 @@ func (s *roleService) GetAllRoles(ctx context.Context) ([]model.Role, error) {
func (s *roleService) GetRole(ctx context.Context, name string) (*model.Role, error) {
return s.roleRepo.GetRole(ctx, name)
}
func (s *roleService) GetRoleDetail(ctx context.Context, name string) (*RoleDetail, error) {
role, err := s.roleRepo.GetRole(ctx, name)
if err != nil {
return nil, apperrors.ErrRoleNotFound
}
// 获取用户数量
userCount, err := s.roleRepo.GetRoleUserCount(ctx, name)
if err != nil {
userCount = 0
}
// 获取权限
permissions, err := s.casbinSvc.GetPermissionsForRole(ctx, name)
if err != nil {
return nil, err
}
perms := make([]Permission, 0)
for _, p := range permissions {
if len(p) >= 2 {
perms = append(perms, Permission{
Resource: p[0],
Action: p[1],
})
}
}
return &RoleDetail{
Name: role.Name,
DisplayName: role.DisplayName,
Description: role.Description,
ParentRole: role.ParentRole,
Priority: role.Priority,
UserCount: userCount,
Permissions: perms,
CreatedAt: role.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: role.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}, nil
}
func (s *roleService) GetRolePermissions(ctx context.Context, name string) ([]Permission, error) {
// 检查角色是否存在
_, err := s.roleRepo.GetRole(ctx, name)
if err != nil {
return nil, apperrors.ErrRoleNotFound
}
permissions, err := s.casbinSvc.GetPermissionsForRole(ctx, name)
if err != nil {
return nil, err
}
// 初始化为空数组,避免 JSON 序列化为 null
perms := make([]Permission, 0)
for _, p := range permissions {
if len(p) >= 2 {
perms = append(perms, Permission{
Resource: p[0],
Action: p[1],
})
}
}
return perms, nil
}
func (s *roleService) GetAllPermissions(ctx context.Context) ([]Permission, error) {
permissions := s.casbinSvc.GetAllPermissions(ctx)
perms := make([]Permission, 0)
for _, p := range permissions {
if len(p) >= 3 {
// p[0] 是角色名p[1] 是资源p[2] 是操作
perms = append(perms, Permission{
Resource: p[1],
Action: p[2],
})
}
}
return perms, nil
}
func (s *roleService) UpdateRolePermissions(ctx context.Context, roleName string, permissions []Permission) error {
// 检查角色是否存在
_, err := s.roleRepo.GetRole(ctx, roleName)
if err != nil {
return apperrors.ErrRoleNotFound
}
// 不能修改超级管理员权限
if roleName == model.RoleSuperAdmin {
return apperrors.ErrCannotModifySuperAdmin
}
// 转换权限格式
var perms [][]string
for _, p := range permissions {
perms = append(perms, []string{p.Resource, p.Action})
}
return s.casbinSvc.UpdatePermissionsForRole(ctx, roleName, perms)
}