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:
@@ -60,6 +60,14 @@ type ProfileService interface {
|
||||
// 批量查询
|
||||
GetByNames(ctx context.Context, names []string) ([]*model.Profile, error)
|
||||
GetByProfileName(ctx context.Context, name string) (*model.Profile, error)
|
||||
|
||||
// ProfileSearchByName 按用户名批量查询档案,返回简化的 NameAndId 列表(文档 2.1)
|
||||
// 规范化(toLowerCase)、空名过滤、每页最多 maxBatch 个限制由本方法在服务端兜底处理。
|
||||
ProfileSearchByName(ctx context.Context, names []string, maxBatch int) ([]model.NameAndId, error)
|
||||
|
||||
// ProfileSearchByNameSingle 按单个用户名查询档案,返回简化的 NameAndId(文档 2.2)
|
||||
// 未找到时返回 (nil, nil),符合文档中"客户端返回 Optional.empty()"的契约。
|
||||
ProfileSearchByNameSingle(ctx context.Context, name string) (*model.NameAndId, error)
|
||||
}
|
||||
|
||||
// TextureService 材质服务接口
|
||||
@@ -138,6 +146,115 @@ type YggdrasilService interface {
|
||||
GetPublicKey(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// FriendActionType 好友操作类型(文档 1.3.2)
|
||||
type FriendActionType string
|
||||
|
||||
const (
|
||||
FriendActionAdd FriendActionType = "ADD"
|
||||
FriendActionRemove FriendActionType = "REMOVE"
|
||||
)
|
||||
|
||||
// FriendActionRequest 好友操作请求(文档 1.3.2)
|
||||
type FriendActionRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ProfileID string `json:"profileId,omitempty"`
|
||||
UpdateType FriendActionType `json:"updateType"`
|
||||
}
|
||||
|
||||
// FriendDTO 好友条目(文档 1.3.1 FriendDto)
|
||||
type FriendDTO struct {
|
||||
ProfileID string `json:"profileId"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// FriendsListResponse 好友列表响应(文档 1.3.1)
|
||||
type FriendsListResponse struct {
|
||||
Friends []FriendDTO `json:"friends"`
|
||||
IncomingRequests []FriendDTO `json:"incomingRequests"`
|
||||
OutgoingRequests []FriendDTO `json:"outgoingRequests"`
|
||||
}
|
||||
|
||||
// PlayerAttributesRequest 玩家偏好更新请求(文档 1.2.2)
|
||||
type PlayerAttributesRequest struct {
|
||||
ProfanityFilterPreferences *ProfanityFilterPreferences `json:"profanityFilterPreferences,omitempty"`
|
||||
FriendsPreferences *FriendsPreferences `json:"friendsPreferences,omitempty"`
|
||||
}
|
||||
|
||||
// ProfanityFilterPreferences 脏词过滤偏好
|
||||
type ProfanityFilterPreferences struct {
|
||||
ProfanityFilterOn *bool `json:"profanityFilterOn,omitempty"`
|
||||
}
|
||||
|
||||
// FriendsPreferences 好友偏好
|
||||
type FriendsPreferences struct {
|
||||
Friends *string `json:"friends,omitempty"` // ENABLED / DISABLED
|
||||
AcceptInvites *string `json:"acceptInvites,omitempty"` // ENABLED / DISABLED
|
||||
}
|
||||
|
||||
// PlayerAttributesResponse 玩家属性响应(文档 1.2.1)
|
||||
// 仅实现好友相关字段,其它(privileges/banStatus 等)不在好友系统范围
|
||||
type PlayerAttributesResponse struct {
|
||||
ProfanityFilterPreferences ProfanityFilterPreferencesResp `json:"profanityFilterPreferences"`
|
||||
FriendsPreferences FriendsPreferencesResp `json:"friendsPreferences"`
|
||||
ChatPreferences ChatPreferencesResp `json:"chatPreferences"`
|
||||
}
|
||||
|
||||
// ProfanityFilterPreferencesResp 脏词过滤偏好响应
|
||||
type ProfanityFilterPreferencesResp struct {
|
||||
ProfanityFilterOn bool `json:"profanityFilterOn"`
|
||||
}
|
||||
|
||||
// FriendsPreferencesResp 好友偏好响应
|
||||
type FriendsPreferencesResp struct {
|
||||
Friends string `json:"friends"`
|
||||
AcceptInvites string `json:"acceptInvites"`
|
||||
}
|
||||
|
||||
// ChatPreferencesResp 聊天偏好响应
|
||||
type ChatPreferencesResp struct {
|
||||
TextCommunication string `json:"textCommunication"`
|
||||
}
|
||||
|
||||
// PresenceRequest 在线状态上报请求(文档 1.1)
|
||||
type PresenceRequest struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// PresenceEntry 单个在线玩家状态(文档 1.1)
|
||||
type PresenceEntry struct {
|
||||
ProfileID string `json:"profileId"`
|
||||
PMID string `json:"pmid,omitempty"`
|
||||
Status string `json:"status"`
|
||||
LastUpdated string `json:"lastUpdated"`
|
||||
}
|
||||
|
||||
// PresenceResponse 在线状态响应(文档 1.1)
|
||||
type PresenceResponse struct {
|
||||
Presence []PresenceEntry `json:"presence"`
|
||||
}
|
||||
|
||||
// BlockListResponse 屏蔽列表响应(文档 1.6)
|
||||
type BlockListResponse struct {
|
||||
BlockedProfiles []string `json:"blockedProfiles"`
|
||||
}
|
||||
|
||||
// FriendsService 好友系统服务接口(文档 1.1 / 1.2 / 1.3 / 1.6)
|
||||
type FriendsService interface {
|
||||
// 好友列表与操作
|
||||
ListFriends(ctx context.Context, requesterUUID string) (*FriendsListResponse, error)
|
||||
PerformFriendAction(ctx context.Context, requesterUUID string, req FriendActionRequest) (*FriendsListResponse, error)
|
||||
|
||||
// 玩家偏好属性
|
||||
GetAttributes(ctx context.Context, profileUUID string) (*PlayerAttributesResponse, error)
|
||||
UpdateAttributes(ctx context.Context, profileUUID string, req PlayerAttributesRequest) error
|
||||
|
||||
// 在线状态
|
||||
UpdatePresence(ctx context.Context, requesterUUID string, status string) (*PresenceResponse, error)
|
||||
|
||||
// 屏蔽列表
|
||||
GetBlocklist(ctx context.Context, blockerUUID string) (*BlockListResponse, error)
|
||||
}
|
||||
|
||||
// SecurityService 安全服务接口
|
||||
type SecurityService interface {
|
||||
// 登录安全
|
||||
@@ -162,6 +279,7 @@ type Services struct {
|
||||
Captcha CaptchaService
|
||||
Yggdrasil YggdrasilService
|
||||
Security SecurityService
|
||||
Friends FriendsService
|
||||
}
|
||||
|
||||
// ServiceDeps 服务依赖
|
||||
|
||||
Reference in New Issue
Block a user