feat(yggdrasil): 实现好友系统与 profiles 查询接口
按 MinecraftServices 文档实现好友系四组接口与 profiles 服务:
好友系统(/api/yggdrasil/minecraftservices/*)
- Friends(1.3):列表查询、ADD/REMOVE 操作(发请求/接受/拒绝/撤回/删除)
- 状态枚举+单向记录模型,接受请求时合并两方向避免好友列表重复
- Player Attributes(1.2):friendsPreferences / 脏词过滤 / 聊天偏好读写
- Upsert 用 map 显式写字段规避 gorm 对带 default 零值布尔字段的忽略
- Presence(1.1):Redis key+TTL 上报与好友在线状态批量查询
- Blocklist(1.6):屏蔽列表查询(含 120s Redis 缓存),Block/Unblock 内部方法
Profiles 服务
- getManyByName(2.1):POST /api/profiles/minecraft,返回 [{id,name}]
- toLowerCase 规范化、去重、空名过滤、maxBatch=10 超限拒绝
- getByName(2.2):GET /api/users/profiles/minecraft/:name,返回 {id,name},未找到返回 404
路由与基础设施
- 路由按官方 host 前缀一一转发
- api.mojang.com/* -> /api/yggdrasil/api/*
- api.minecraftservices.com/* -> /api/yggdrasil/minecraftservices/*
- 新增 Friend/PlayerAttributes 模型与 AutoMigrate 注册
- 新增 FriendsService 与 Container 装配
- 抽 extractBearerToken helper,统一 MinecraftServices 错误响应
修复
- texture_service: ToggleFavorite 在 db 为 nil 时降级非事务执行
修复 TestTextureServiceImpl_ToggleFavorite 空指针 panic
- texture_service_test: UploadTexture 用例改用真实 SHA-256 命中 mock
修复 4 个子用例因文件大小/Hash 不匹配的失败
- profile_repository: GetByNames/FindByName 改 LOWER() 大小写不敏感
与客户端 toLowerCase 规范化对齐
测试
- 好友模型、ADD/REMOVE/接受、属性、屏蔽、Bearer 解析单测全绿
- profiles 查询规范化、去重、超限、大小写不敏感单测全绿
- go build ./... && go test ./... 全部通过
This commit is contained in:
511
internal/service/yggdrasil_friends_service.go
Normal file
511
internal/service/yggdrasil_friends_service.go
Normal file
@@ -0,0 +1,511 @@
|
||||
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{}
|
||||
Reference in New Issue
Block a user