topfans/backend/services/socialService/service/friend_service.go
2026-05-16 02:42:32 +08:00

1277 lines
42 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"errors"
"fmt"
"time"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pbCommon "github.com/topfans/backend/pkg/proto/common"
pb "github.com/topfans/backend/pkg/proto/social"
"github.com/topfans/backend/services/socialService/config"
"github.com/topfans/backend/services/socialService/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
// FriendService 好友服务接口
type FriendService interface {
// ========== 好友请求相关 ==========
// SendFriendRequest 发送好友请求
SendFriendRequest(ctx context.Context, req *pb.SendFriendRequestRequest, userID, starID int64) (*pb.SendFriendRequestResponse, error)
// GetFriendRequests 获取好友请求列表
GetFriendRequests(ctx context.Context, req *pb.GetFriendRequestsRequest, userID, starID int64) (*pb.GetFriendRequestsResponse, error)
// HandleFriendRequest 处理好友请求(接受/拒绝)
HandleFriendRequest(ctx context.Context, req *pb.HandleFriendRequestRequest, userID, starID int64) (*pb.HandleFriendRequestResponse, error)
// ========== 好友关系相关 ==========
// GetFriendList 获取好友列表
GetFriendList(ctx context.Context, req *pb.GetFriendListRequest, userID, starID int64) (*pb.GetFriendListResponse, error)
// DeleteFriend 删除好友
DeleteFriend(ctx context.Context, req *pb.DeleteFriendRequest, userID, starID int64) (*pb.DeleteFriendResponse, error)
// SetFriendRemark 设置好友备注
SetFriendRemark(ctx context.Context, req *pb.SetFriendRemarkRequest, userID, starID int64) (*pb.SetFriendRemarkResponse, error)
// CheckFriendship 检查好友关系
CheckFriendship(ctx context.Context, req *pb.CheckFriendshipRequest) (*pb.CheckFriendshipResponse, error)
// GetFriendCount 获取好友数量
GetFriendCount(ctx context.Context, req *pb.GetFriendCountRequest, userID, starID int64) (*pb.GetFriendCountResponse, error)
// SearchUserForFriend 查找用户信息(用于加好友)
SearchUserForFriend(ctx context.Context, req *pb.SearchUserForFriendRequest, userID, starID int64) (*pb.SearchUserForFriendResponse, error)
// ========== 随机用户相关 ==========
// GetRandomUsers 获取随机用户(同一明星下)
GetRandomUsers(ctx context.Context, req *pb.GetRandomUsersRequest, userID, starID int64) (*pb.GetRandomUsersResponse, error)
// ========== 分页用户相关 ==========
// GetUsersPaged 获取分页用户列表(同一明星身份下)
GetUsersPaged(ctx context.Context, req *pb.GetUsersPagedRequest, userID, starID int64) (*pb.GetUsersPagedResponse, error)
}
// friendService 好友服务实现
type friendService struct {
socialRepo repository.SocialRepository
userClient UserServiceClient // RPC客户端用于调用userService
db *gorm.DB
config *config.SocialConfig
}
// NewFriendService 创建好友服务实例
func NewFriendService(
socialRepo repository.SocialRepository,
userClient UserServiceClient,
db *gorm.DB,
) FriendService {
return &friendService{
socialRepo: socialRepo,
userClient: userClient,
db: db,
config: config.GlobalSocialConfig,
}
}
// ========== 好友请求相关实现 ==========
// SendFriendRequest 发送好友请求(支持按昵称搜索)
func (s *friendService) SendFriendRequest(ctx context.Context, req *pb.SendFriendRequestRequest, userID, starID int64) (*pb.SendFriendRequestResponse, error) {
// 1. 如果是搜索模式search_mode=true 且提供了昵称),返回匹配的用户列表
if req.SearchMode && req.Nickname != "" {
return s.searchUsersByNickname(ctx, req, userID, starID)
}
// 2. 参数验证 - 正常发送请求模式
if req.FriendUserId == 0 {
logger.Logger.Warn("Invalid friend_user_id", zap.Int64("friend_user_id", req.FriendUserId))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidFriendUserID),
}, nil
}
if userID == req.FriendUserId {
logger.Logger.Warn("Cannot add self as friend", zap.Int64("user_id", userID))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrCannotAddSelf),
}, nil
}
// 3. 验证对方用户是否存在
exists, err := s.userClient.ValidateUser(ctx, req.FriendUserId)
if err != nil {
logger.Logger.Error("Failed to validate user", zap.Int64("friend_user_id", req.FriendUserId), zap.Error(err))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if !exists {
logger.Logger.Warn("Friend user not found", zap.Int64("friend_user_id", req.FriendUserId))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrUserNotFound),
}, nil
}
// 4. 验证对方的粉丝档案是否存在(必须是同一个明星的粉丝)
exists, err = s.userClient.ValidateFanProfile(ctx, req.FriendUserId, starID)
if err != nil {
logger.Logger.Error("Failed to validate fan profile", zap.Int64("friend_user_id", req.FriendUserId), zap.Int64("star_id", starID), zap.Error(err))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if !exists {
logger.Logger.Warn("Friend user is not a fan of this star", zap.Int64("friend_user_id", req.FriendUserId), zap.Int64("star_id", starID))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrNotFanOfStar),
}, nil
}
// 4. 检查是否已经是好友
isFriend, err := s.socialRepo.CheckFriendship(userID, req.FriendUserId, starID)
if err != nil {
logger.Logger.Error("Failed to check friendship", zap.Int64("user_id", userID), zap.Int64("friend_user_id", req.FriendUserId), zap.Error(err))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if isFriend {
logger.Logger.Warn("Already friends", zap.Int64("user_id", userID), zap.Int64("friend_user_id", req.FriendUserId))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrAlreadyFriends),
}, nil
}
// 5. 检查是否有待处理的请求
latestRequest, err := s.socialRepo.GetLatestRequest(userID, req.FriendUserId, starID)
if err != nil {
logger.Logger.Error("Failed to get latest request", zap.Error(err))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if latestRequest != nil {
// 5.1 如果有待处理的请求
if latestRequest.Status == models.FriendRequestStatusPending {
logger.Logger.Warn("Request already pending", zap.Int64("request_id", latestRequest.ID))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrRequestAlreadyPending),
}, nil
}
// 5.2 如果最近被拒绝,检查是否在冷却期内
if latestRequest.Status == models.FriendRequestStatusRejected {
if latestRequest.ProcessedAt != nil && s.config.IsInCooldownPeriod(*latestRequest.ProcessedAt) {
remainingDays := s.config.CalculateRemainingCooldownDays(*latestRequest.ProcessedAt)
logger.Logger.Warn("Request in cooldown period",
zap.Int64("user_id", userID),
zap.Int64("friend_user_id", req.FriendUserId),
zap.Int("remaining_days", remainingDays),
)
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponseWithMessage(
pbCommon.StatusCode_STATUS_TOO_MANY_REQUESTS,
fmt.Sprintf("请求已被拒绝,请 %d 天后再试", remainingDays),
),
}, nil
}
}
}
// 6. 创建好友请求
now := time.Now().UnixMilli()
expiresAt := s.config.CalculateExpiryTime(now)
friendRequest := &models.FriendRequest{
FromUserID: userID,
ToUserID: req.FriendUserId,
StarID: starID,
Message: &req.Message,
Status: models.FriendRequestStatusPending,
ExpiresAt: &expiresAt,
}
if err := s.socialRepo.CreateRequest(friendRequest); err != nil {
logger.Logger.Error("Failed to create friend request", zap.Error(err))
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Friend request created",
zap.Int64("request_id", friendRequest.ID),
zap.Int64("from_user_id", userID),
zap.Int64("to_user_id", req.FriendUserId),
zap.Int64("star_id", starID),
)
// 7. 构建响应
return &pb.SendFriendRequestResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "好友请求已发送",
Timestamp: time.Now().UnixMilli(),
},
RequestId: friendRequest.ID,
Status: friendRequest.Status,
CreatedAt: friendRequest.CreatedAt,
ExpiresAt: *friendRequest.ExpiresAt,
}, nil
}
// GetFriendRequests 获取好友请求列表
func (s *friendService) GetFriendRequests(ctx context.Context, req *pb.GetFriendRequestsRequest, userID, starID int64) (*pb.GetFriendRequestsResponse, error) {
// 1. 参数验证
if req.Type != models.FriendRequestTypeReceived && req.Type != models.FriendRequestTypeSent {
logger.Logger.Warn("Invalid request type", zap.String("type", req.Type))
return &pb.GetFriendRequestsResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidAction),
}, nil
}
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 50 {
pageSize = 50 // 限制最大页大小
}
// 2. 查询请求列表
requests, total, err := s.socialRepo.GetRequestsByUser(userID, starID, req.Type, req.Status, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("Failed to get requests", zap.Error(err))
return &pb.GetFriendRequestsResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 3. 填充用户信息
items, err := s.fillRequestUserInfo(ctx, requests, req.Type, starID)
if err != nil {
logger.Logger.Error("Failed to fill user info", zap.Error(err))
return &pb.GetFriendRequestsResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 4. 构建响应
return &pb.GetFriendRequestsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// HandleFriendRequest 处理好友请求(接受/拒绝)
func (s *friendService) HandleFriendRequest(ctx context.Context, req *pb.HandleFriendRequestRequest, userID, starID int64) (*pb.HandleFriendRequestResponse, error) {
// 1. 参数验证
if req.RequestId == 0 {
logger.Logger.Warn("Invalid request_id", zap.Int64("request_id", req.RequestId))
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponseWithMessage(pbCommon.StatusCode_STATUS_BAD_REQUEST, "request_id不能为空"),
}, nil
}
if req.Action != "accept" && req.Action != "reject" {
logger.Logger.Warn("Invalid action", zap.String("action", req.Action))
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidAction),
}, nil
}
// 2. 查询请求
friendRequest, err := s.socialRepo.GetRequestByID(req.RequestId)
if err != nil {
logger.Logger.Error("Failed to get request", zap.Int64("request_id", req.RequestId), zap.Error(err))
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if friendRequest == nil {
logger.Logger.Warn("Request not found", zap.Int64("request_id", req.RequestId))
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrFriendRequestNotFound),
}, nil
}
// 3. 验证权限(只有接收者可以处理)
if friendRequest.ToUserID != userID {
logger.Logger.Warn("User is not the receiver",
zap.Int64("user_id", userID),
zap.Int64("to_user_id", friendRequest.ToUserID),
)
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrCannotProcessOwnRequest),
}, nil
}
// 4. 验证状态只能处理pending状态的请求
if friendRequest.Status != models.FriendRequestStatusPending {
logger.Logger.Warn("Request is not pending",
zap.Int64("request_id", req.RequestId),
zap.String("status", friendRequest.Status),
)
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrRequestAlreadyProcessed),
}, nil
}
// 5. 检查是否已过期
if friendRequest.ExpiresAt != nil && s.config.IsExpired(*friendRequest.ExpiresAt) {
logger.Logger.Warn("Request expired", zap.Int64("request_id", req.RequestId))
// 更新状态为已过期
processedAt := time.Now().UnixMilli()
_ = s.socialRepo.UpdateRequestStatus(req.RequestId, models.FriendRequestStatusExpired, &processedAt)
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrRequestExpired),
}, nil
}
// 6. 检查是否已经是好友处理并发场景A和B互相发送请求其中一个已被接受
isFriend, err := s.socialRepo.CheckFriendship(friendRequest.FromUserID, friendRequest.ToUserID, starID)
if err != nil {
logger.Logger.Error("Failed to check friendship", zap.Error(err))
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if isFriend {
// 已经是好友了,将此请求标记为已接受(避免重复处理)
processedAt := time.Now().UnixMilli()
_ = s.socialRepo.UpdateRequestStatus(req.RequestId, models.FriendRequestStatusAccepted, &processedAt)
logger.Logger.Info("Users are already friends, marking request as accepted",
zap.Int64("request_id", req.RequestId),
zap.Int64("from_user_id", friendRequest.FromUserID),
zap.Int64("to_user_id", friendRequest.ToUserID),
)
return &pb.HandleFriendRequestResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "你们已经是好友了",
Timestamp: time.Now().UnixMilli(),
},
Action: req.Action,
FriendshipCreated: false, // 好友关系之前就已存在
ProcessedAt: processedAt,
}, nil
}
// 7. 处理请求
processedAt := time.Now().UnixMilli()
var newStatus string
var friendshipCreated bool = false
if req.Action == "accept" {
// 7.1 接受请求
newStatus = models.FriendRequestStatusAccepted
// 7.1.0 检查双方好友数量是否已达上限
maxFriends := config.GetMaxFriends()
fromCount, _ := s.socialRepo.CountFriends(friendRequest.FromUserID, starID)
toCount, _ := s.socialRepo.CountFriends(friendRequest.ToUserID, starID)
if fromCount >= int64(maxFriends) {
logger.Logger.Warn("From user reached max friends limit",
zap.Int64("from_user_id", friendRequest.FromUserID),
zap.Int64("current_count", fromCount),
zap.Int("max_friends", maxFriends),
)
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponseWithMessage(
pbCommon.StatusCode_STATUS_FORBIDDEN,
fmt.Sprintf("对方好友数量已达上限(最多%d个", maxFriends),
),
Action: req.Action,
FriendshipCreated: false,
ProcessedAt: processedAt,
}, nil
}
if toCount >= int64(maxFriends) {
logger.Logger.Warn("To user reached max friends limit",
zap.Int64("to_user_id", friendRequest.ToUserID),
zap.Int64("current_count", toCount),
zap.Int("max_friends", maxFriends),
)
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponseWithMessage(
pbCommon.StatusCode_STATUS_FORBIDDEN,
fmt.Sprintf("你的好友数量已达上限(最多%d个无法接受新好友", maxFriends),
),
Action: req.Action,
FriendshipCreated: false,
ProcessedAt: processedAt,
}, nil
}
// 使用事务确保原子性
err = s.db.Transaction(func(tx *gorm.DB) error {
// 7.1.1 更新请求状态
if err := s.socialRepo.UpdateRequestStatus(req.RequestId, newStatus, &processedAt); err != nil {
return fmt.Errorf("更新请求状态失败: %w", err)
}
// 7.1.2 创建双向好友关系
if err := s.socialRepo.CreateFriendshipPair(friendRequest.FromUserID, friendRequest.ToUserID, starID); err != nil {
return fmt.Errorf("创建好友关系失败: %w", err)
}
// 7.1.3 更新双方的social字段好友数量
// TODO: 调用userService更新fan_profiles表的social字段
// 暂时省略,后续实现
return nil
})
if err != nil {
logger.Logger.Error("Failed to accept friend request", zap.Error(err))
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
friendshipCreated = true
logger.Logger.Info("Friend request accepted",
zap.Int64("request_id", req.RequestId),
zap.Int64("from_user_id", friendRequest.FromUserID),
zap.Int64("to_user_id", friendRequest.ToUserID),
)
} else {
// 7.2 拒绝请求
newStatus = models.FriendRequestStatusRejected
if err := s.socialRepo.UpdateRequestStatus(req.RequestId, newStatus, &processedAt); err != nil {
logger.Logger.Error("Failed to reject friend request", zap.Error(err))
return &pb.HandleFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Friend request rejected",
zap.Int64("request_id", req.RequestId),
zap.Int64("from_user_id", friendRequest.FromUserID),
zap.Int64("to_user_id", friendRequest.ToUserID),
)
}
// 8. 构建响应
return &pb.HandleFriendRequestResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Action: req.Action,
FriendshipCreated: friendshipCreated,
ProcessedAt: processedAt,
}, nil
}
// ========== 好友关系相关实现 ==========
// GetFriendList 获取好友列表
func (s *friendService) GetFriendList(ctx context.Context, req *pb.GetFriendListRequest, userID, starID int64) (*pb.GetFriendListResponse, error) {
// 1. 参数验证
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 50 {
pageSize = 50 // 限制最大页大小
}
// 2. 查询好友列表
friendships, total, err := s.socialRepo.GetFriendsByUser(userID, starID, req.Keyword, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("Failed to get friends", zap.Error(err))
return &pb.GetFriendListResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 3. 填充好友信息
items, err := s.fillFriendshipUserInfo(ctx, friendships, starID)
if err != nil {
logger.Logger.Error("Failed to fill friend info", zap.Error(err))
return &pb.GetFriendListResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 4. 构建响应
return &pb.GetFriendListResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// DeleteFriend 删除好友
func (s *friendService) DeleteFriend(ctx context.Context, req *pb.DeleteFriendRequest, userID, starID int64) (*pb.DeleteFriendResponse, error) {
// 1. 参数验证
if req.FriendUserId == 0 {
logger.Logger.Warn("Invalid friend_user_id", zap.Int64("friend_user_id", req.FriendUserId))
return &pb.DeleteFriendResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidFriendUserID),
}, nil
}
// 2. 检查是否为好友
isFriend, err := s.socialRepo.CheckFriendship(userID, req.FriendUserId, starID)
if err != nil {
logger.Logger.Error("Failed to check friendship", zap.Error(err))
return &pb.DeleteFriendResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if !isFriend {
logger.Logger.Warn("Not friends", zap.Int64("user_id", userID), zap.Int64("friend_user_id", req.FriendUserId))
return &pb.DeleteFriendResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrNotFriends),
}, nil
}
// 3. 删除双向好友关系(使用事务)
if err := s.socialRepo.DeleteFriendshipPair(userID, req.FriendUserId, starID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Logger.Warn("Friendship not found", zap.Int64("user_id", userID), zap.Int64("friend_user_id", req.FriendUserId))
return &pb.DeleteFriendResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrNotFriends),
}, nil
}
logger.Logger.Error("Failed to delete friendship", zap.Error(err))
return &pb.DeleteFriendResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 4. 更新双方的social字段好友数量
// TODO: 调用userService更新fan_profiles表的social字段
// 暂时省略,后续实现
logger.Logger.Info("Friend deleted",
zap.Int64("user_id", userID),
zap.Int64("friend_user_id", req.FriendUserId),
zap.Int64("star_id", starID),
)
// 5. 构建响应
return &pb.DeleteFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "已删除好友",
Timestamp: time.Now().UnixMilli(),
},
Success: true,
}, nil
}
// SetFriendRemark 设置好友备注
func (s *friendService) SetFriendRemark(ctx context.Context, req *pb.SetFriendRemarkRequest, userID, starID int64) (*pb.SetFriendRemarkResponse, error) {
// 1. 参数验证
if req.FriendUserId == 0 {
logger.Logger.Warn("Invalid friend_user_id", zap.Int64("friend_user_id", req.FriendUserId))
return &pb.SetFriendRemarkResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidFriendUserID),
}, nil
}
// 验证备注长度
if len(req.Remark) > 50 {
logger.Logger.Warn("Remark too long", zap.String("remark", req.Remark))
return &pb.SetFriendRemarkResponse{
Base: appErrors.BuildBaseResponseWithMessage(pbCommon.StatusCode_STATUS_BAD_REQUEST, "备注长度不能超过50个字符"),
}, nil
}
// 2. 检查是否为好友
isFriend, err := s.socialRepo.CheckFriendship(userID, req.FriendUserId, starID)
if err != nil {
logger.Logger.Error("Failed to check friendship", zap.Error(err))
return &pb.SetFriendRemarkResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
if !isFriend {
logger.Logger.Warn("Not friends", zap.Int64("user_id", userID), zap.Int64("friend_user_id", req.FriendUserId))
return &pb.SetFriendRemarkResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrNotFriends),
}, nil
}
// 3. 更新备注
if err := s.socialRepo.UpdateRemark(userID, req.FriendUserId, starID, req.Remark); err != nil {
logger.Logger.Error("Failed to update remark", zap.Error(err))
return &pb.SetFriendRemarkResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
logger.Logger.Info("Friend remark updated",
zap.Int64("user_id", userID),
zap.Int64("friend_user_id", req.FriendUserId),
zap.String("remark", req.Remark),
)
// 4. 构建响应
return &pb.SetFriendRemarkResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "备注已更新",
Timestamp: time.Now().UnixMilli(),
},
Remark: req.Remark,
}, nil
}
// CheckFriendship 检查好友关系
func (s *friendService) CheckFriendship(ctx context.Context, req *pb.CheckFriendshipRequest) (*pb.CheckFriendshipResponse, error) {
// 1. 参数验证
if req.UserId == 0 || req.FriendUserId == 0 {
logger.Logger.Warn("Invalid user IDs",
zap.Int64("user_id", req.UserId),
zap.Int64("friend_user_id", req.FriendUserId),
)
return &pb.CheckFriendshipResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID),
}, nil
}
// 注意这里没有starID参数需要从上下文或其他地方获取
// 暂时使用0后续需要修改
starID := int64(0) // TODO: 从上下文获取starID
// 2. 检查好友关系
isFriend, err := s.socialRepo.CheckFriendship(req.UserId, req.FriendUserId, starID)
if err != nil {
logger.Logger.Error("Failed to check friendship", zap.Error(err))
return &pb.CheckFriendshipResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 3. 构建响应
return &pb.CheckFriendshipResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
IsFriend: isFriend,
}, nil
}
// GetFriendCount 获取好友数量
func (s *friendService) GetFriendCount(ctx context.Context, req *pb.GetFriendCountRequest, userID, starID int64) (*pb.GetFriendCountResponse, error) {
// 1. 统计好友数量
count, err := s.socialRepo.CountFriends(userID, starID)
if err != nil {
logger.Logger.Error("Failed to count friends", zap.Error(err))
return &pb.GetFriendCountResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 2. 构建响应
return &pb.GetFriendCountResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Count: count,
}, nil
}
// ========== 辅助方法 ==========
// fillRequestUserInfo 填充请求的用户信息
func (s *friendService) fillRequestUserInfo(ctx context.Context, requests []*models.FriendRequest, requestType string, starID int64) ([]*pb.FriendRequest, error) {
if len(requests) == 0 {
return []*pb.FriendRequest{}, nil
}
// 收集需要查询的用户ID
userIDs := make([]int64, 0, len(requests)*2)
for _, req := range requests {
userIDs = append(userIDs, req.FromUserID, req.ToUserID)
}
// 批量查询用户信息和粉丝档案
userInfoMap, err := s.userClient.GetUsersByIDs(ctx, userIDs, starID)
if err != nil {
return nil, fmt.Errorf("批量查询用户信息失败: %w", err)
}
// 填充信息
items := make([]*pb.FriendRequest, 0, len(requests))
for _, req := range requests {
item := &pb.FriendRequest{
Id: req.ID,
FromUserId: req.FromUserID,
ToUserId: req.ToUserID,
StarId: req.StarID,
Message: getStringValue(req.Message),
Status: req.Status,
CreatedAt: req.CreatedAt,
ExpiresAt: getInt64Value(req.ExpiresAt),
ProcessedAt: getInt64Value(req.ProcessedAt),
}
// 填充发送者信息
if fromUser, ok := userInfoMap[req.FromUserID]; ok {
item.FromUserNickname = fromUser.Nickname
item.FromUserAvatar = fromUser.Avatar
item.FromUserFanLevel = fromUser.FanLevel
}
// 填充接收者信息
if toUser, ok := userInfoMap[req.ToUserID]; ok {
item.ToUserNickname = toUser.Nickname
item.ToUserAvatar = toUser.Avatar
item.ToUserFanLevel = toUser.FanLevel
}
items = append(items, item)
}
return items, nil
}
// fillFriendshipUserInfo 填充好友关系的用户信息
func (s *friendService) fillFriendshipUserInfo(ctx context.Context, friendships []*models.Friendship, starID int64) ([]*pb.Friendship, error) {
if len(friendships) == 0 {
return []*pb.Friendship{}, nil
}
// 收集需要查询的好友ID
friendIDs := make([]int64, 0, len(friendships))
for _, f := range friendships {
friendIDs = append(friendIDs, f.FriendID)
}
// 批量查询好友信息和粉丝档案
userInfoMap, err := s.userClient.GetUsersByIDs(ctx, friendIDs, starID)
if err != nil {
return nil, fmt.Errorf("批量查询用户信息失败: %w", err)
}
// 填充信息
items := make([]*pb.Friendship, 0, len(friendships))
for _, f := range friendships {
item := &pb.Friendship{
Id: f.ID,
UserId: f.UserID,
FriendId: f.FriendID,
StarId: f.StarID,
Status: f.Status,
Remark: getStringValue(f.Remark),
Intimacy: f.Intimacy,
CreatedAt: f.CreatedAt,
}
// 填充好友信息
if friendUser, ok := userInfoMap[f.FriendID]; ok {
item.FriendNickname = friendUser.Nickname
item.FriendAvatar = friendUser.Avatar
item.FriendFanLevel = friendUser.FanLevel
item.FriendSocial = friendUser.Social
}
items = append(items, item)
}
return items, nil
}
// getStringValue 获取字符串指针的值如果为nil返回空字符串
func getStringValue(s *string) string {
if s == nil {
return ""
}
return *s
}
// getInt64Value 获取int64指针的值如果为nil返回0
func getInt64Value(i *int64) int64 {
if i == nil {
return 0
}
return *i
}
// SearchUserForFriend 查找粉丝档案信息(用于加好友)
func (s *friendService) SearchUserForFriend(ctx context.Context, req *pb.SearchUserForFriendRequest, userID, starID int64) (*pb.SearchUserForFriendResponse, error) {
// 1. 参数验证
friendFanProfileID := req.GetFriendFanProfileId()
if friendFanProfileID == 0 {
logger.Logger.Warn("Invalid friend_fan_profile_id", zap.Int64("friend_fan_profile_id", friendFanProfileID))
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_BAD_REQUEST,
Message: "friend_fan_profile_id不能为空",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 2. 查询当前用户的粉丝档案(用于后续比较和查询关系)
var myFanProfile models.FanProfile
if err := s.db.WithContext(ctx).Where("user_id = ? AND star_id = ? AND is_active = ?", userID, starID, true).First(&myFanProfile).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Logger.Warn("Current user's fan profile not found",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_NOT_FOUND,
Message: "当前用户的粉丝档案不存在",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Error("Failed to get current user's fan profile",
zap.Int64("user_id", userID),
zap.Error(err),
)
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "获取当前用户信息失败",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 3. 查询目标粉丝档案
var fanProfile models.FanProfile
if err := s.db.WithContext(ctx).Where("id = ? AND is_active = ?", friendFanProfileID, true).First(&fanProfile).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Logger.Warn("Fan profile not found",
zap.Int64("friend_fan_profile_id", friendFanProfileID),
)
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_NOT_FOUND,
Message: "粉丝档案不存在",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Error("Failed to get fan profile",
zap.Int64("friend_fan_profile_id", friendFanProfileID),
zap.Error(err),
)
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "获取粉丝档案失败",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 4. 不能查找自己
if friendFanProfileID == myFanProfile.ID {
logger.Logger.Warn("Cannot search for self", zap.Int64("my_fan_profile_id", myFanProfile.ID))
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_BAD_REQUEST,
Message: "不能查找自己",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 5. 检查是否在同一明星下(不能跨明星加好友)
if fanProfile.StarID != starID {
logger.Logger.Warn("Cannot add friend across different stars",
zap.Int64("current_star_id", starID),
zap.Int64("target_star_id", fanProfile.StarID),
)
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_BAD_REQUEST,
Message: "不能添加其他明星的粉丝为好友",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 6. 查询好友关系状态使用粉丝档案ID
relationshipStatus, canSendRequest, cooldownEndsAt := s.determineRelationshipStatus(ctx, myFanProfile.ID, friendFanProfileID, starID)
// 6. 构建响应
avatar := ""
if fanProfile.AvatarURL != nil {
avatar = *fanProfile.AvatarURL
}
userResult := &pb.FanProfileSearchResult{
FanProfileId: fanProfile.ID,
UserId: fanProfile.UserID,
Nickname: fanProfile.Nickname,
Avatar: avatar,
FanLevel: fanProfile.Level,
RelationshipStatus: relationshipStatus,
CanSendRequest: canSendRequest,
CooldownEndsAt: cooldownEndsAt,
}
logger.Logger.Info("SearchUserForFriend successful",
zap.Int64("user_id", userID),
zap.Int64("friend_fan_profile_id", friendFanProfileID),
zap.String("relationship_status", relationshipStatus),
)
return &pb.SearchUserForFriendResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
User: userResult,
}, nil
}
// determineRelationshipStatus 判断与目标用户的关系状态
// 返回值relationshipStatus, canSendRequest, cooldownEndsAt
func (s *friendService) determineRelationshipStatus(ctx context.Context, userID, friendUserID, starID int64) (string, bool, int64) {
// 1. 检查是否已经是好友
isFriend, err := s.socialRepo.CheckFriendship(userID, friendUserID, starID)
if err != nil {
logger.Logger.Error("Failed to check friendship",
zap.Int64("user_id", userID),
zap.Int64("friend_user_id", friendUserID),
zap.Error(err),
)
}
if isFriend {
return "friend", false, 0
}
// 2. 检查是否有待处理的好友请求
// 2.1 检查我发送给对方的请求
sentRequest, err := s.socialRepo.GetLatestRequest(userID, friendUserID, starID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logger.Logger.Error("Failed to get sent friend request",
zap.Int64("from_user_id", userID),
zap.Int64("to_user_id", friendUserID),
zap.Error(err),
)
}
if sentRequest != nil && sentRequest.Status == models.FriendRequestStatusPending {
// 我已发送请求,等待对方处理
return "pending_sent", false, 0
}
// 2.2 检查对方发送给我的请求
receivedRequest, err := s.socialRepo.GetLatestRequest(friendUserID, userID, starID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logger.Logger.Error("Failed to get received friend request",
zap.Int64("from_user_id", friendUserID),
zap.Int64("to_user_id", userID),
zap.Error(err),
)
}
if receivedRequest != nil && receivedRequest.Status == models.FriendRequestStatusPending {
// 对方已发送请求给我,等待我处理
return "pending_received", false, 0
}
// 3. 检查是否在冷静期(被拒绝后的冷静期)
if sentRequest != nil && sentRequest.Status == models.FriendRequestStatusRejected && sentRequest.ProcessedAt != nil {
cooldownPeriod := time.Duration(s.config.TimeConstraints.RejectionCooldownDays*24) * time.Hour
processedTime := time.UnixMilli(*sentRequest.ProcessedAt)
cooldownEndsAt := processedTime.Add(cooldownPeriod)
if time.Now().Before(cooldownEndsAt) {
// 在冷静期内
return "rejected", false, cooldownEndsAt.UnixMilli()
}
}
// 4. 陌生人,可以发送好友请求
return "stranger", true, 0
}
// searchUsersByNickname 按昵称搜索用户(搜索模式)
func (s *friendService) searchUsersByNickname(ctx context.Context, req *pb.SendFriendRequestRequest, userID, starID int64) (*pb.SendFriendRequestResponse, error) {
// 1. 参数验证
nickname := req.Nickname
if nickname == "" {
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponseWithMessage(pbCommon.StatusCode_STATUS_BAD_REQUEST, "昵称不能为空"),
}, nil
}
// 昵称长度限制
if len(nickname) < 1 || len(nickname) > 50 {
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponseWithMessage(pbCommon.StatusCode_STATUS_BAD_REQUEST, "昵称长度需在1-50个字符之间"),
}, nil
}
// 2. 搜索匹配的用户
profiles, err := s.socialRepo.GetFanProfilesByNickname(starID, nickname, 20)
if err != nil {
logger.Logger.Error("Failed to search users by nickname",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("nickname", nickname),
zap.Error(err),
)
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 3. 获取当前用户的粉丝档案ID
myFanProfile, err := s.socialRepo.GetFanProfileByUserIDAndStarID(userID, starID)
if err != nil {
logger.Logger.Error("Failed to get current user's fan profile",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
return &pb.SendFriendRequestResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 4. 转换为搜索结果并排除自己
matchedUsers := make([]*pb.FanProfileSearchResult, 0, len(profiles))
for _, profile := range profiles {
// 排除自己
if profile.UserID == userID {
continue
}
// 判断关系状态
relationshipStatus, canSendRequest, cooldownEndsAt := s.determineRelationshipStatus(ctx, myFanProfile.ID, profile.ID, starID)
avatar := ""
if profile.AvatarURL != nil {
avatar = *profile.AvatarURL
}
matchedUsers = append(matchedUsers, &pb.FanProfileSearchResult{
FanProfileId: profile.ID,
UserId: profile.UserID,
Nickname: profile.Nickname,
Avatar: avatar,
FanLevel: profile.Level,
RelationshipStatus: relationshipStatus,
CanSendRequest: canSendRequest,
CooldownEndsAt: cooldownEndsAt,
})
}
logger.Logger.Info("SearchUsersByNickname successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("nickname", nickname),
zap.Int("matched_count", len(matchedUsers)),
)
// 5. 构建响应
return &pb.SendFriendRequestResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
MatchedUsers: matchedUsers,
}, nil
}
// ========== 随机用户相关实现 ==========
// GetRandomUsers 获取随机用户(同一明星下)
func (s *friendService) GetRandomUsers(ctx context.Context, req *pb.GetRandomUsersRequest, userID, starID int64) (*pb.GetRandomUsersResponse, error) {
// 1. 参数验证
if starID <= 0 {
logger.Logger.Warn("Invalid star_id", zap.Int64("star_id", starID))
return &pb.GetRandomUsersResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidStarID),
}, nil
}
// 2. 确定返回数量
count := int(req.Count)
if count <= 0 {
count = 1 // 默认返回1个
}
if count > 100 {
count = 100 // 最大限制100个
}
// 3. 调用 repository 获取随机用户
randomUsers, err := s.socialRepo.GetRandomUsersByStar(starID, count)
if err != nil {
logger.Logger.Error("Failed to get random users",
zap.Int64("star_id", starID),
zap.Int("count", count),
zap.Error(err),
)
return &pb.GetRandomUsersResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 4. 转换为 Protobuf 消息
pbUsers := make([]*pb.RandomUser, 0, len(randomUsers))
for _, user := range randomUsers {
pbUsers = append(pbUsers, &pb.RandomUser{
UserId: user.UserID,
Nickname: user.Nickname,
})
}
// 5. 构建响应
return &pb.GetRandomUsersResponse{
Base: appErrors.BuildBaseResponse(nil),
Users: pbUsers,
}, nil
}
// GetUsersPaged 获取分页用户列表(同一明星身份下),当前用户始终在第一位
func (s *friendService) GetUsersPaged(ctx context.Context, req *pb.GetUsersPagedRequest, userID, starID int64) (*pb.GetUsersPagedResponse, error) {
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
// 不排除任何用户,获取所有用户(包括当前用户)
users, total, err := s.socialRepo.GetUsersByStarPaged(starID, 0, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("GetUsersByStarPaged failed",
zap.Int64("star_id", starID),
zap.Int64("user_id", userID),
zap.Error(err),
)
return &pb.GetUsersPagedResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
}, nil
}
// 获取当前用户的档案信息
var currentUserProfile *models.FanProfile
currentUserProfile, err = s.socialRepo.GetFanProfileByUserIDAndStarID(userID, starID)
if err != nil {
logger.Logger.Warn("GetFanProfileByUserIDAndStarID failed, user may not have profile",
zap.Int64("star_id", starID),
zap.Int64("user_id", userID),
zap.Error(err),
)
}
// 收集所有用户ID
allUserIDs := make([]int64, 0, len(users))
for _, user := range users {
allUserIDs = append(allUserIDs, user.UserID)
}
// 批量查询展位占用数量
slotCounts, err := s.socialRepo.GetSlotCountsByUsers(allUserIDs, starID)
if err != nil {
logger.Logger.Warn("GetSlotCountsByUsers failed",
zap.Int64("star_id", starID),
zap.Error(err),
)
// 不影响主流程使用空map
slotCounts = make(map[int64]int64)
}
pbUsers := make([]*pb.PagedUser, 0, len(users))
isFirstPage := page == 1
for _, user := range users {
// 如果是第一页且是当前用户,跳过(稍后单独插入到第一位)
if isFirstPage && user.UserID == userID {
continue
}
occupiedSlots := slotCounts[user.UserID]
remainingSlots := int32(user.SlotLimit) - int32(occupiedSlots)
if remainingSlots < 0 {
remainingSlots = 0
}
pbUsers = append(pbUsers, &pb.PagedUser{
UserId: user.UserID,
GalleryOwnerId: user.UserID,
Nickname: user.Nickname,
Level: user.Level,
SharedBoothSlotsRemaining: remainingSlots,
})
}
// 如果是第一页,将当前用户插入到列表第一位
if isFirstPage && currentUserProfile != nil {
occupiedSlots := slotCounts[currentUserProfile.UserID]
remainingSlots := int32(currentUserProfile.SlotLimit) - int32(occupiedSlots)
if remainingSlots < 0 {
remainingSlots = 0
}
pbUsers = append([]*pb.PagedUser{{
UserId: currentUserProfile.UserID,
GalleryOwnerId: currentUserProfile.UserID,
Nickname: currentUserProfile.Nickname,
Level: currentUserProfile.Level,
SharedBoothSlotsRemaining: remainingSlots,
}}, pbUsers...)
}
return &pb.GetUsersPagedResponse{
Base: appErrors.BuildBaseResponse(nil),
Users: pbUsers,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}