feat(group): refactor group invite workflow for improved user experience

- Simplify InviteMembers to always send invite card directly to invitee
- Modify RespondInvite to check group join type (anyone vs need approval) before adding member
- Update SetGroupAddRequest to properly handle invite flow with invitee agreement and admin approval stages
- Add case-insensitive search using LOWER() in user repository
- Add random offset (0-50) for first page of recommended posts
- Update dependencies: remove gorilla/websocket, promote gomail to direct dependency
This commit is contained in:
2026-03-12 23:40:21 +08:00
parent 0a0cbacbcc
commit 1216423350
5 changed files with 138 additions and 76 deletions

View File

@@ -423,7 +423,7 @@ func (h *MessageHandler) HandleSendMessage(c *gin.Context) {
response.BadRequest(c, "detail_type is required")
return
}
if params.Segments == nil || len(params.Segments) == 0 {
if len(params.Segments) == 0 {
response.BadRequest(c, "segments is required")
return
}

View File

@@ -2,6 +2,7 @@ package repository
import (
"carrot_bbs/internal/model"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@@ -341,8 +342,12 @@ func (r *UserRepository) Search(keyword string, page, pageSize int) ([]*model.Us
keyword,
)
} else {
searchPattern := "%" + keyword + "%"
query = query.Where("username LIKE ? OR nickname LIKE ? OR bio LIKE ?", searchPattern, searchPattern, searchPattern)
// SQLite: 使用 LOWER() 实现大小写不敏感的模糊搜索
lowerSearchPattern := "%" + strings.ToLower(keyword) + "%"
query = query.Where(
"LOWER(username) LIKE ? OR LOWER(nickname) LIKE ? OR LOWER(bio) LIKE ?",
lowerSearchPattern, lowerSearchPattern, lowerSearchPattern,
)
}
}

View File

@@ -649,7 +649,6 @@ func (s *groupService) InviteMembers(userID string, groupID string, memberIDs []
if !isInviterMember {
return ErrNotGroupMember
}
isInviterAdmin := s.IsGroupAdmin(userID, groupID)
inviter, _ := s.userRepo.GetByID(userID)
inviterName := "群成员"
@@ -685,7 +684,7 @@ func (s *groupService) InviteMembers(userID string, groupID string, memberIDs []
if _, err := s.requestRepo.GetPendingByGroupAndTarget(groupID, memberID, model.GroupJoinRequestTypeInvite); err == nil {
continue
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
expireAt := time.Now().Add(72 * time.Hour)
@@ -702,42 +701,9 @@ func (s *groupService) InviteMembers(userID string, groupID string, memberIDs []
continue
}
if isInviterAdmin {
// 群主/管理员邀请:直接发送邀请卡片,等待被邀请人确认
s.sendGroupInviteToTarget(group, req, inviterName, inviterAvatar)
createdCount++
continue
}
inviteeName := "用户"
inviteeAvatar := ""
if invitee, e := s.userRepo.GetByID(memberID); e == nil && invitee != nil {
if invitee.Nickname != "" {
inviteeName = invitee.Nickname
}
inviteeAvatar = invitee.Avatar
}
reviewerIDs := s.collectGroupReviewerIDs(group.ID, group.OwnerID, userID)
for _, reviewerID := range reviewerIDs {
s.createSystemNotification(reviewerID, model.SysNotifyGroupJoinApply, inviterName+" 邀请 "+inviteeName+" 加入群聊 "+group.Name+",请审批", &model.SystemNotificationExtra{
ActorIDStr: userID,
ActorName: inviterName,
AvatarURL: inviterAvatar,
TargetID: req.Flag,
TargetTitle: group.Name,
TargetType: string(model.GroupJoinRequestTypeInvite),
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
GroupDescription: group.Description,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(req.Status),
TargetUserID: memberID,
TargetUserName: inviteeName,
TargetUserAvatar: inviteeAvatar,
})
}
// 无论是否需要审批,都先发送邀请卡片给被邀请人
// 被邀请人同意后,再根据群的策略决定是否需要管理员审批
s.sendGroupInviteToTarget(group, req, inviterName, inviterAvatar)
createdCount++
}
@@ -792,7 +758,7 @@ func (s *groupService) JoinGroup(userID string, groupID string) error {
applicantAvatar,
)
return ErrJoinRequestPending
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
@@ -861,22 +827,81 @@ func (s *groupService) RespondInvite(userID string, flag string, approve bool, r
}
now := time.Now()
req.ReviewerID = userID
req.ReviewedAt = &now
req.Reason = reason
if approve {
if err := s.addMemberToGroupAndConversation(group, userID, userID); err != nil {
return err
// 检查群的加群策略
if group.JoinType == model.JoinTypeAnyone {
// 允许任何人加入,直接进群
if err := s.addMemberToGroupAndConversation(group, userID, userID); err != nil {
return err
}
req.Status = model.GroupJoinRequestStatusAccepted
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你发出的群邀请已被接受", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
} else {
// 需要审批或不允许加入时,被邀请人同意后通知管理员审批
// 设置 ReviewerID 为被邀请人表示被邀请人已同意
req.ReviewerID = userID
req.ReviewedAt = &now
// 获取被邀请人信息
targetUserName := "用户"
targetUserAvatar := ""
if u, e := s.userRepo.GetByID(userID); e == nil && u != nil {
if u.Nickname != "" {
targetUserName = u.Nickname
}
targetUserAvatar = u.Avatar
}
// 获取邀请人信息
inviterName := "群成员"
inviterAvatar := ""
if u, e := s.userRepo.GetByID(req.InitiatorID); e == nil && u != nil {
if u.Nickname != "" {
inviterName = u.Nickname
}
inviterAvatar = u.Avatar
}
// 通知管理员审批
reviewerIDs := s.collectGroupReviewerIDs(group.ID, group.OwnerID, req.InitiatorID)
for _, reviewerID := range reviewerIDs {
s.createSystemNotification(reviewerID, model.SysNotifyGroupJoinApply, inviterName+" 邀请 "+targetUserName+" 加入群聊 "+group.Name+",被邀请人已同意,请审批", &model.SystemNotificationExtra{
ActorIDStr: req.InitiatorID,
ActorName: inviterName,
AvatarURL: inviterAvatar,
TargetID: req.Flag,
TargetTitle: group.Name,
TargetType: string(model.GroupJoinRequestTypeInvite),
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
GroupDescription: group.Description,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(req.Status),
TargetUserID: userID,
TargetUserName: targetUserName,
TargetUserAvatar: targetUserAvatar,
})
}
// 通知邀请人:被邀请人已同意,等待管理员审批
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, targetUserName+" 已接受你的群邀请,等待管理员审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(req.Status),
})
}
req.Status = model.GroupJoinRequestStatusAccepted
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你发出的群邀请已被接受", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
} else {
req.Status = model.GroupJoinRequestStatusRejected
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你发出的群邀请已被拒绝", &model.SystemNotificationExtra{
@@ -906,9 +931,18 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
if req.Status != model.GroupJoinRequestStatusPending {
return ErrGroupRequestHandled
}
if req.RequestType == model.GroupJoinRequestTypeInvite && req.ReviewerID != "" {
// invite 类型中 reviewerID 非空表示已完成管理员审批,等待被邀请人确认
return ErrGroupRequestHandled
// 检查 invite 类型的请求:
// - ReviewerID 非空表示被邀请人已同意,等待管理员审批
// - ReviewerID 为空表示被邀请人还未同意,管理员不能提前审批
if req.RequestType == model.GroupJoinRequestTypeInvite {
if req.ReviewerID == "" {
// 被邀请人还未同意,管理员不能提前审批
return &ServiceError{Code: 400, Message: "被邀请人尚未同意邀请,无法审批"}
}
if req.ReviewerID != req.TargetUserID {
// ReviewerID 不是被邀请人,说明已经被其他人处理过
return ErrGroupRequestHandled
}
}
if !s.IsGroupAdmin(userID, req.GroupID) {
return ErrNotGroupAdmin
@@ -923,8 +957,7 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
}
now := time.Now()
req.ReviewerID = userID
req.ReviewedAt = &now
// 保存管理员ID到ReviewerID覆盖被邀请人的ID
req.Reason = reason
targetUserName := "用户"
if u, e := s.userRepo.GetByID(req.TargetUserID); e == nil && u != nil && u.Nickname != "" {
@@ -941,29 +974,36 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
if approve {
if req.RequestType == model.GroupJoinRequestTypeInvite {
// 管理员审批通过后,不直接拉人;发送邀请卡片等待对方确认
inviterName := "群成员"
inviterAvatar := ""
if u, e := s.userRepo.GetByID(req.InitiatorID); e == nil && u != nil {
if u.Nickname != "" {
inviterName = u.Nickname
}
inviterAvatar = u.Avatar
// 被邀请人已同意,管理员审批通过后直接进群
if err := s.addMemberToGroupAndConversation(group, req.TargetUserID, userID); err != nil {
return err
}
s.sendGroupInviteToTarget(group, req, inviterName, inviterAvatar)
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你邀请 "+targetUserName+" 入群已通过审批,等待对方确认", &model.SystemNotificationExtra{
req.Status = model.GroupJoinRequestStatusAccepted
req.ReviewerID = userID
req.ReviewedAt = &now
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupJoinApproved, "你接受的群邀请已通过审批,你已加入群聊 "+group.Name, &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusPending),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你邀请 "+targetUserName+" 入群已通过审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
} else {
if err := s.addMemberToGroupAndConversation(group, req.TargetUserID, userID); err != nil {
return err
}
req.Status = model.GroupJoinRequestStatusAccepted
req.ReviewerID = userID
req.ReviewedAt = &now
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupJoinApproved, "你申请加入群聊 "+group.Name+" 已通过", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
@@ -996,8 +1036,19 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
}
} else {
req.Status = model.GroupJoinRequestStatusRejected
req.ReviewerID = userID
req.ReviewedAt = &now
if req.RequestType == model.GroupJoinRequestTypeInvite {
// 成员邀请被管理员拒绝,通知邀请人
// 被邀请人已同意但管理员拒绝,通知被邀请人和邀请人
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupJoinRejected, "你接受的群邀请未通过审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusRejected),
Reason: reason,
})
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你邀请 "+targetUserName+" 入群未通过审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log"
"math/rand"
"strings"
"time"
@@ -255,7 +256,7 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
}
cacheKey := cache.PostListKey("latest", visibilityUserKey, page, pageSize)
result, err := cache.GetOrLoadTyped[*PostListResult](
result, err := cache.GetOrLoadTyped(
s.cache,
cacheKey,
postListTTL,
@@ -595,8 +596,14 @@ func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, pa
return s.GetHotPosts(ctx, page, pageSize)
}
// 计算偏移量
offset := (page - 1) * pageSize
// 计算偏移量:第一页使用随机偏移量,增加推荐多样性
var offset int
if page == 1 {
// 第一页随机偏移 0-50让每次刷新看到不同的推荐内容
offset = rand.Intn(51)
} else {
offset = (page - 1) * pageSize
}
// 从Gorse获取推荐列表
// 多取1条用于判断是否还有下一页