512 lines
17 KiB
Go
512 lines
17 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
apperrors "carrotskin/internal/errors"
|
|||
|
|
"carrotskin/internal/model"
|
|||
|
|
"carrotskin/internal/repository"
|
|||
|
|
"carrotskin/pkg/redis"
|
|||
|
|
"carrotskin/pkg/utils"
|
|||
|
|
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
// PresenceKeyPrefix 在线状态 Redis 键前缀,key 形如 presence:<profileUUID>
|
|||
|
|
PresenceKeyPrefix = "presence:"
|
|||
|
|
// PresenceTTL 在线状态过期时间;客户端最长 5 分钟必发一次心跳(文档 1.1)
|
|||
|
|
PresenceTTL = 5 * time.Minute
|
|||
|
|
// BlocklistCacheTTL 屏蔽列表缓存冷却时间(文档 1.6:客户端 120 秒节流)
|
|||
|
|
BlocklistCacheTTL = 120 * time.Second
|
|||
|
|
// BlocklistCacheKeyPrefix 屏蔽列表缓存键前缀
|
|||
|
|
BlocklistCacheKeyPrefix = "blocklist:"
|
|||
|
|
|
|||
|
|
// PresenceStatusOffline 离线状态固定值(文档 1.1)
|
|||
|
|
PresenceStatusOffline = "OFFLINE"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// friendsService 好友系统服务实现
|
|||
|
|
type friendsService struct {
|
|||
|
|
friendRepo repository.FriendRepository
|
|||
|
|
attrRepo repository.PlayerAttributeRepository
|
|||
|
|
profileRepo repository.ProfileRepository
|
|||
|
|
redis *redis.Client
|
|||
|
|
logger *zap.Logger
|
|||
|
|
presenceTTL time.Duration
|
|||
|
|
blocklistTTL time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewFriendsService 创建好友系统服务实例
|
|||
|
|
func NewFriendsService(
|
|||
|
|
friendRepo repository.FriendRepository,
|
|||
|
|
attrRepo repository.PlayerAttributeRepository,
|
|||
|
|
profileRepo repository.ProfileRepository,
|
|||
|
|
redisClient *redis.Client,
|
|||
|
|
logger *zap.Logger,
|
|||
|
|
) FriendsService {
|
|||
|
|
return &friendsService{
|
|||
|
|
friendRepo: friendRepo,
|
|||
|
|
attrRepo: attrRepo,
|
|||
|
|
profileRepo: profileRepo,
|
|||
|
|
redis: redisClient,
|
|||
|
|
logger: logger,
|
|||
|
|
presenceTTL: PresenceTTL,
|
|||
|
|
blocklistTTL: BlocklistCacheTTL,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================================
|
|||
|
|
// 好友列表与操作
|
|||
|
|
// ============================================================================
|
|||
|
|
|
|||
|
|
// ListFriends 获取好友列表(文档 1.3.1)
|
|||
|
|
func (s *friendsService) ListFriends(ctx context.Context, requesterUUID string) (*FriendsListResponse, error) {
|
|||
|
|
accepted, err := s.friendRepo.ListAccepted(ctx, requesterUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询好友列表失败: %w", err)
|
|||
|
|
}
|
|||
|
|
incoming, err := s.friendRepo.ListIncoming(ctx, requesterUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询收到的请求失败: %w", err)
|
|||
|
|
}
|
|||
|
|
outgoing, err := s.friendRepo.ListOutgoing(ctx, requesterUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询发出的请求失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量解析 profile name
|
|||
|
|
friends, err := s.toFriendDTOs(ctx, accepted)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
incomingDTOs, err := s.toFriendDTOs(ctx, incoming)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
outgoingDTOs, err := s.toFriendDTOs(ctx, outgoing)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &FriendsListResponse{
|
|||
|
|
Friends: friends,
|
|||
|
|
IncomingRequests: incomingDTOs,
|
|||
|
|
OutgoingRequests: outgoingDTOs,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PerformFriendAction 执行好友加/删操作(文档 1.3.2)
|
|||
|
|
func (s *friendsService) PerformFriendAction(ctx context.Context, requesterUUID string, req FriendActionRequest) (*FriendsListResponse, error) {
|
|||
|
|
switch req.UpdateType {
|
|||
|
|
case FriendActionAdd:
|
|||
|
|
if err := s.addFriend(ctx, requesterUUID, req); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
case FriendActionRemove:
|
|||
|
|
if err := s.removeFriend(ctx, requesterUUID, req); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
default:
|
|||
|
|
return nil, apperrors.NewBadRequest("无效的 updateType", nil)
|
|||
|
|
}
|
|||
|
|
// 文档 1.3.2:PUT 成功后返回最新好友列表
|
|||
|
|
return s.ListFriends(ctx, requesterUUID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// addFriend 添加好友 / 发送请求 / 接受请求(文档 1.3.2 ADD)
|
|||
|
|
func (s *friendsService) addFriend(ctx context.Context, requesterUUID string, req FriendActionRequest) error {
|
|||
|
|
targetUUID, err := s.resolveTargetUUID(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 校验目标存在
|
|||
|
|
targetProfile, err := s.profileRepo.FindByUUID(ctx, targetUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return apperrors.NewBadRequest(apperrors.FriendsErrUnknownProfile, nil)
|
|||
|
|
}
|
|||
|
|
return fmt.Errorf("查询目标档案失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if targetProfile == nil {
|
|||
|
|
return apperrors.NewBadRequest(apperrors.FriendsErrUnknownProfile, nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 不能添加自己
|
|||
|
|
if requesterUUID == targetUUID {
|
|||
|
|
return apperrors.NewBadRequest(apperrors.FriendsErrCannotAddSelf, nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查 requester->target 已有记录
|
|||
|
|
existing, err := s.friendRepo.FindRelation(ctx, requesterUUID, targetUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("查询已有关系失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if existing != nil {
|
|||
|
|
switch existing.Status {
|
|||
|
|
case model.FriendsStatusAccepted:
|
|||
|
|
// 已是好友,幂等返回成功
|
|||
|
|
return nil
|
|||
|
|
case model.FriendsStatusBlocked:
|
|||
|
|
// requester 屏蔽了 target,解除屏蔽并建好友
|
|||
|
|
if err := s.friendRepo.Delete(ctx, existing.ID); err != nil {
|
|||
|
|
return fmt.Errorf("解除屏蔽失败: %w", err)
|
|||
|
|
}
|
|||
|
|
case model.FriendsStatusPending:
|
|||
|
|
// 已发过请求,幂等返回
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查 target->requester 是否已向我发出 pending 请求 -> 自动接受
|
|||
|
|
reverse, err := s.friendRepo.FindRelation(ctx, targetUUID, requesterUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("查询反向关系失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if reverse != nil {
|
|||
|
|
switch reverse.Status {
|
|||
|
|
case model.FriendsStatusPending:
|
|||
|
|
// 对方已向我发起请求,直接互为好友(更新 A→B 为 accepted 即可,
|
|||
|
|
// ListAccepted 会合并两方向,无需补建 B→A 以避免好友列表重复)
|
|||
|
|
if err := s.friendRepo.UpdateStatus(ctx, reverse.ID, model.FriendsStatusAccepted); err != nil {
|
|||
|
|
return fmt.Errorf("接受好友请求失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
case model.FriendsStatusAccepted:
|
|||
|
|
// 对方已记录为好友(数据修复场景),确保 requester->target 也存在 accepted 记录
|
|||
|
|
if existing == nil {
|
|||
|
|
if err := s.friendRepo.Create(ctx, &model.Friend{
|
|||
|
|
RequesterUUID: requesterUUID,
|
|||
|
|
TargetUUID: targetUUID,
|
|||
|
|
Status: model.FriendsStatusAccepted,
|
|||
|
|
}); err != nil {
|
|||
|
|
return fmt.Errorf("创建好友记录失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
case model.FriendsStatusBlocked:
|
|||
|
|
// 对方屏蔽了我,不接受好友请求
|
|||
|
|
return apperrors.NewForbidden("对方已屏蔽你")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 普通发送请求
|
|||
|
|
if existing == nil {
|
|||
|
|
if err := s.friendRepo.Create(ctx, &model.Friend{
|
|||
|
|
RequesterUUID: requesterUUID,
|
|||
|
|
TargetUUID: targetUUID,
|
|||
|
|
Status: model.FriendsStatusPending,
|
|||
|
|
}); err != nil {
|
|||
|
|
return fmt.Errorf("发送好友请求失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// removeFriend 删除好友 / 拒绝请求 / 撤回请求(文档 1.3.2 REMOVE)
|
|||
|
|
func (s *friendsService) removeFriend(ctx context.Context, requesterUUID string, req FriendActionRequest) error {
|
|||
|
|
targetUUID, err := s.resolveTargetUUID(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除 requester->target 记录(涵盖撤回发出的请求、删除好友)
|
|||
|
|
if rel, err := s.friendRepo.FindRelation(ctx, requesterUUID, targetUUID); err != nil {
|
|||
|
|
return fmt.Errorf("查询关系失败: %w", err)
|
|||
|
|
} else if rel != nil {
|
|||
|
|
if err := s.friendRepo.Delete(ctx, rel.ID); err != nil {
|
|||
|
|
return fmt.Errorf("删除关系失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 拒绝收到的请求时(requester 作为 target 方),删除 target->requester 的 pending 记录
|
|||
|
|
if rel, err := s.friendRepo.FindRelation(ctx, targetUUID, requesterUUID); err != nil {
|
|||
|
|
return fmt.Errorf("查询反向关系失败: %w", err)
|
|||
|
|
} else if rel != nil && rel.Status == model.FriendsStatusPending {
|
|||
|
|
if err := s.friendRepo.Delete(ctx, rel.ID); err != nil {
|
|||
|
|
return fmt.Errorf("拒绝请求失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// resolveTargetUUID 解析请求目标 UUID:profileId 优先,否则用 name 查 Profile
|
|||
|
|
func (s *friendsService) resolveTargetUUID(ctx context.Context, req FriendActionRequest) (string, error) {
|
|||
|
|
if req.ProfileID != "" {
|
|||
|
|
return utils.FormatUUIDToNoDash(req.ProfileID), nil
|
|||
|
|
}
|
|||
|
|
if req.Name != "" {
|
|||
|
|
profile, err := s.profileRepo.FindByName(ctx, req.Name)
|
|||
|
|
if err != nil {
|
|||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return "", apperrors.NewBadRequest(apperrors.FriendsErrUnknownProfile, nil)
|
|||
|
|
}
|
|||
|
|
return "", fmt.Errorf("查询目标档案失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if profile == nil {
|
|||
|
|
return "", apperrors.NewBadRequest(apperrors.FriendsErrUnknownProfile, nil)
|
|||
|
|
}
|
|||
|
|
return profile.UUID, nil
|
|||
|
|
}
|
|||
|
|
return "", apperrors.NewBadRequest(apperrors.FriendsErrUnknownProfile, nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// toFriendDTOs 将 UUID 列表批量解析为 FriendDTO(补 name)
|
|||
|
|
func (s *friendsService) toFriendDTOs(ctx context.Context, uuids []string) ([]FriendDTO, error) {
|
|||
|
|
if len(uuids) == 0 {
|
|||
|
|
return []FriendDTO{}, nil
|
|||
|
|
}
|
|||
|
|
profiles, err := s.profileRepo.FindByUUIDs(ctx, uuids)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("批量查询档案失败: %w", err)
|
|||
|
|
}
|
|||
|
|
nameByUUID := make(map[string]string, len(profiles))
|
|||
|
|
for _, p := range profiles {
|
|||
|
|
if p != nil {
|
|||
|
|
nameByUUID[p.UUID] = p.Name
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
result := make([]FriendDTO, 0, len(uuids))
|
|||
|
|
for _, uid := range uuids {
|
|||
|
|
result = append(result, FriendDTO{
|
|||
|
|
ProfileID: uid,
|
|||
|
|
Name: nameByUUID[uid], // 找不到 name 时留空
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================================
|
|||
|
|
// 玩家偏好属性
|
|||
|
|
// ============================================================================
|
|||
|
|
|
|||
|
|
// GetAttributes 获取玩家属性(文档 1.2.1)
|
|||
|
|
func (s *friendsService) GetAttributes(ctx context.Context, profileUUID string) (*PlayerAttributesResponse, error) {
|
|||
|
|
attr, err := s.attrRepo.Get(ctx, profileUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询玩家偏好失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if attr == nil {
|
|||
|
|
d := model.DefaultPlayerAttributes(profileUUID)
|
|||
|
|
attr = &d
|
|||
|
|
}
|
|||
|
|
return &PlayerAttributesResponse{
|
|||
|
|
ProfanityFilterPreferences: ProfanityFilterPreferencesResp{
|
|||
|
|
ProfanityFilterOn: attr.ProfanityFilterOn,
|
|||
|
|
},
|
|||
|
|
FriendsPreferences: FriendsPreferencesResp{
|
|||
|
|
Friends: enabledToString(attr.FriendsEnabled),
|
|||
|
|
AcceptInvites: enabledToString(attr.AcceptInvites),
|
|||
|
|
},
|
|||
|
|
ChatPreferences: ChatPreferencesResp{
|
|||
|
|
TextCommunication: attr.TextCommunication,
|
|||
|
|
},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UpdateAttributes 更新玩家偏好(文档 1.2.2)
|
|||
|
|
func (s *friendsService) UpdateAttributes(ctx context.Context, profileUUID string, req PlayerAttributesRequest) error {
|
|||
|
|
// 先读取现有值(保留未传字段)
|
|||
|
|
attr, err := s.attrRepo.Get(ctx, profileUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("查询玩家偏好失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if attr == nil {
|
|||
|
|
d := model.DefaultPlayerAttributes(profileUUID)
|
|||
|
|
attr = &d
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if req.ProfanityFilterPreferences != nil && req.ProfanityFilterPreferences.ProfanityFilterOn != nil {
|
|||
|
|
attr.ProfanityFilterOn = *req.ProfanityFilterPreferences.ProfanityFilterOn
|
|||
|
|
}
|
|||
|
|
if req.FriendsPreferences != nil {
|
|||
|
|
if req.FriendsPreferences.Friends != nil {
|
|||
|
|
attr.FriendsEnabled = *req.FriendsPreferences.Friends == "ENABLED"
|
|||
|
|
}
|
|||
|
|
if req.FriendsPreferences.AcceptInvites != nil {
|
|||
|
|
attr.AcceptInvites = *req.FriendsPreferences.AcceptInvites == "ENABLED"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := s.attrRepo.Upsert(ctx, attr); err != nil {
|
|||
|
|
return fmt.Errorf("更新玩家偏好失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// enabledToString 将布尔偏好转为 ENABLED/DISABLED 字符串
|
|||
|
|
func enabledToString(enabled bool) string {
|
|||
|
|
if enabled {
|
|||
|
|
return "ENABLED"
|
|||
|
|
}
|
|||
|
|
return "DISABLED"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================================
|
|||
|
|
// 在线状态
|
|||
|
|
// ============================================================================
|
|||
|
|
|
|||
|
|
// UpdatePresence 上报自身在线状态并返回好友在线状态(文档 1.1)
|
|||
|
|
func (s *friendsService) UpdatePresence(ctx context.Context, requesterUUID string, status string) (*PresenceResponse, error) {
|
|||
|
|
// 写入自身状态到 Redis
|
|||
|
|
key := PresenceKeyPrefix + requesterUUID
|
|||
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|||
|
|
// 存储 "status|lastUpdated",便于读取时一并恢复时间戳
|
|||
|
|
value := status + "|" + now
|
|||
|
|
if err := s.redis.Set(ctx, key, value, s.presenceTTL); err != nil {
|
|||
|
|
s.logger.Error("写入在线状态失败",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.String("profileUUID", requesterUUID),
|
|||
|
|
)
|
|||
|
|
return nil, fmt.Errorf("写入在线状态失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 拉取好友 UUID 列表
|
|||
|
|
friendUUIDs, err := s.friendRepo.ListAccepted(ctx, requesterUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询好友列表失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
entries := s.batchGetPresence(ctx, friendUUIDs)
|
|||
|
|
// 文档 1.1:服务器仅返回非离线玩家
|
|||
|
|
filtered := make([]PresenceEntry, 0, len(entries))
|
|||
|
|
for _, e := range entries {
|
|||
|
|
if e.Status != "" && e.Status != PresenceStatusOffline {
|
|||
|
|
filtered = append(filtered, e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return &PresenceResponse{Presence: filtered}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// batchGetPresence 批量读取多个 profileUUID 的在线状态
|
|||
|
|
func (s *friendsService) batchGetPresence(ctx context.Context, uuids []string) []PresenceEntry {
|
|||
|
|
if len(uuids) == 0 || s.redis == nil {
|
|||
|
|
return []PresenceEntry{}
|
|||
|
|
}
|
|||
|
|
keys := make([]string, 0, len(uuids))
|
|||
|
|
for _, uid := range uuids {
|
|||
|
|
keys = append(keys, PresenceKeyPrefix+uid)
|
|||
|
|
}
|
|||
|
|
// 使用底层 go-redis 客户端的 MGet 一次读取
|
|||
|
|
results, err := s.redis.Client.MGet(ctx, keys...).Result()
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Warn("批量读取在线状态失败", zap.Error(err))
|
|||
|
|
return []PresenceEntry{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
entries := make([]PresenceEntry, 0, len(uuids))
|
|||
|
|
for i, uid := range uuids {
|
|||
|
|
var entry PresenceEntry
|
|||
|
|
entry.ProfileID = uid
|
|||
|
|
if i < len(results) && results[i] != nil {
|
|||
|
|
if raw, ok := results[i].(string); ok && raw != "" {
|
|||
|
|
parts := strings.SplitN(raw, "|", 2)
|
|||
|
|
entry.Status = parts[0]
|
|||
|
|
if len(parts) == 2 {
|
|||
|
|
entry.LastUpdated = parts[1]
|
|||
|
|
} else {
|
|||
|
|
entry.LastUpdated = time.Now().UTC().Format(time.RFC3339Nano)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
entries = append(entries, entry)
|
|||
|
|
}
|
|||
|
|
return entries
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================================
|
|||
|
|
// 屏蔽列表
|
|||
|
|
// ============================================================================
|
|||
|
|
|
|||
|
|
// GetBlocklist 获取屏蔽列表(文档 1.6,含 120s 客户端节流缓存)
|
|||
|
|
func (s *friendsService) GetBlocklist(ctx context.Context, blockerUUID string) (*BlockListResponse, error) {
|
|||
|
|
// 文档 1.6:客户端 120s 节流;服务端在此也做一层缓存,避免重复查库
|
|||
|
|
cacheKey := BlocklistCacheKeyPrefix + blockerUUID
|
|||
|
|
if s.redis != nil {
|
|||
|
|
if cached, err := s.redis.GetBytes(ctx, cacheKey); err == nil && len(cached) > 0 {
|
|||
|
|
var resp BlockListResponse
|
|||
|
|
if err := json.Unmarshal(cached, &resp); err == nil {
|
|||
|
|
return &resp, nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
blocked, err := s.friendRepo.ListBlocked(ctx, blockerUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("查询屏蔽列表失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if blocked == nil {
|
|||
|
|
blocked = []string{}
|
|||
|
|
}
|
|||
|
|
resp := &BlockListResponse{BlockedProfiles: blocked}
|
|||
|
|
|
|||
|
|
// 写入缓存
|
|||
|
|
if s.redis != nil {
|
|||
|
|
if data, err := json.Marshal(resp); err == nil {
|
|||
|
|
if err := s.redis.Set(ctx, cacheKey, data, s.blocklistTTL); err != nil {
|
|||
|
|
s.logger.Warn("缓存屏蔽列表失败", zap.Error(err))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return resp, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 屏蔽/取消屏蔽方法(供后续/管理端调用,当前协议未定义对应端点)
|
|||
|
|
|
|||
|
|
// Block 屏蔽目标玩家
|
|||
|
|
func (s *friendsService) Block(ctx context.Context, blockerUUID, targetUUID string) error {
|
|||
|
|
if blockerUUID == targetUUID {
|
|||
|
|
return apperrors.NewBadRequest(apperrors.FriendsErrCannotAddSelf, nil)
|
|||
|
|
}
|
|||
|
|
// 若已有关系记录,更新为 blocked;否则新建
|
|||
|
|
if rel, err := s.friendRepo.FindRelation(ctx, blockerUUID, targetUUID); err != nil {
|
|||
|
|
return fmt.Errorf("查询关系失败: %w", err)
|
|||
|
|
} else if rel != nil {
|
|||
|
|
if err := s.friendRepo.UpdateStatus(ctx, rel.ID, model.FriendsStatusBlocked); err != nil {
|
|||
|
|
return fmt.Errorf("更新屏蔽状态失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return s.invalidateBlocklistCache(ctx, blockerUUID)
|
|||
|
|
}
|
|||
|
|
if err := s.friendRepo.Create(ctx, &model.Friend{
|
|||
|
|
RequesterUUID: blockerUUID,
|
|||
|
|
TargetUUID: targetUUID,
|
|||
|
|
Status: model.FriendsStatusBlocked,
|
|||
|
|
}); err != nil {
|
|||
|
|
return fmt.Errorf("创建屏蔽记录失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return s.invalidateBlocklistCache(ctx, blockerUUID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Unblock 取消屏蔽
|
|||
|
|
func (s *friendsService) Unblock(ctx context.Context, blockerUUID, targetUUID string) error {
|
|||
|
|
rel, err := s.friendRepo.FindRelation(ctx, blockerUUID, targetUUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("查询关系失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if rel == nil || rel.Status != model.FriendsStatusBlocked {
|
|||
|
|
// 幂等:未屏蔽时直接返回
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if err := s.friendRepo.Delete(ctx, rel.ID); err != nil {
|
|||
|
|
return fmt.Errorf("取消屏蔽失败: %w", err)
|
|||
|
|
}
|
|||
|
|
return s.invalidateBlocklistCache(ctx, blockerUUID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// invalidateBlocklistCache 清除屏蔽列表缓存
|
|||
|
|
func (s *friendsService) invalidateBlocklistCache(ctx context.Context, blockerUUID string) error {
|
|||
|
|
if s.redis == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return s.redis.Del(ctx, BlocklistCacheKeyPrefix+blockerUUID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保引用了 go-redis(MGet 通过 s.redis.Client)
|
|||
|
|
var _ = redis.Client{}
|