Security fixes: - Fix sensitive data exposure: UserResponse no longer contains email/phone - Add UserSelfResponse for authenticated user's own data - Protect Gorse endpoints with admin role requirement - Add rate limiting (10 req/min) to auth endpoints - Fix CORS configuration to use allowed origins list - Add pagination parameter validation (max 100 per page) Changes: - dto/dto.go: Add UserSelfResponse type - dto/user_converter.go: Add ConvertUserToSelfResponse - handler/user_handler.go: Use secure response types - middleware/cors.go: Implement origin whitelist - middleware/ratelimit.go: Enhance rate limiter - router/router.go: Add auth rate limit, protect Gorse - service/admin_*.go: Use ConvertUserToResponse
380 lines
11 KiB
Go
380 lines
11 KiB
Go
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.ConvertUserToResponse(owner)
|
||
}
|
||
}
|
||
|
||
// 获取帖子数量
|
||
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
|
||
}
|