Files
backend/internal/service/interfaces.go
lan f2b02682c2
Some checks failed
Build / build-docker (push) Has been cancelled
Build / build (push) Has been cancelled
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 ./... 全部通过
2026-07-09 20:44:41 +08:00

290 lines
11 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 定义业务逻辑层接口
package service
import (
"context"
"time"
"carrotskin/internal/model"
"carrotskin/pkg/storage"
"go.uber.org/zap"
)
// UserService 用户服务接口
type UserService interface {
// 用户认证
Register(ctx context.Context, username, password, email, avatar string) (*model.User, string, error)
Login(ctx context.Context, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error)
// 用户查询
GetByID(ctx context.Context, id int64) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error)
GetByUsername(ctx context.Context, username string) (*model.User, error)
// 用户更新
UpdateInfo(ctx context.Context, user *model.User) error
UpdateAvatar(ctx context.Context, userID int64, avatarURL string) error
ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error
ResetPassword(ctx context.Context, email, newPassword string) error
ChangeEmail(ctx context.Context, userID int64, newEmail string) error
// 头像上传
UploadAvatar(ctx context.Context, userID int64, fileData []byte, fileName string) (string, error)
// URL验证
ValidateAvatarURL(ctx context.Context, avatarURL string) error
// 配置获取
GetMaxProfilesPerUser() int
GetMaxTexturesPerUser() int
// 管理员操作
ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error)
SetRole(ctx context.Context, userID int64, role string) error
SetStatus(ctx context.Context, userID int64, status int16) error
}
// ProfileService 档案服务接口
type ProfileService interface {
// 档案CRUD
Create(ctx context.Context, userID int64, name string) (*model.Profile, error)
GetByUUID(ctx context.Context, uuid string) (*model.Profile, error)
GetByUserID(ctx context.Context, userID int64) ([]*model.Profile, error)
Update(ctx context.Context, uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error)
Delete(ctx context.Context, uuid string, userID int64) error
// 档案状态
CheckLimit(ctx context.Context, userID int64, maxProfiles int) error
// 批量查询
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 材质服务接口
type TextureService interface {
// 材质CRUD
UploadTexture(ctx context.Context, uploaderID int64, name, description, textureType string, fileData []byte, fileName string, isPublic, isSlim bool) (*model.Texture, error)
GetByID(ctx context.Context, id int64) (*model.Texture, error)
GetByHash(ctx context.Context, hash string) (*model.Texture, error)
GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
Search(ctx context.Context, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
Update(ctx context.Context, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error)
Delete(ctx context.Context, textureID, uploaderID int64) error
// 收藏
ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error)
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
// 限制检查
CheckUploadLimit(ctx context.Context, uploaderID int64, maxTextures int) error
// 管理员操作
ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error)
AdminDelete(ctx context.Context, textureID int64) error
IncrementDownload(ctx context.Context, textureID int64) error
}
// TokenService 令牌服务接口
type TokenService interface {
// 令牌管理
Create(ctx context.Context, userID int64, uuid, clientToken string) (*model.Profile, []*model.Profile, string, string, error)
CreateWithProfile(ctx context.Context, userID int64, profileUUID string, clientToken string) (*model.Profile, []*model.Profile, string, string, error)
Validate(ctx context.Context, accessToken, clientToken string) bool
Refresh(ctx context.Context, accessToken, clientToken, selectedProfileID string) (string, string, error)
Invalidate(ctx context.Context, accessToken string)
InvalidateUserTokens(ctx context.Context, userID int64)
// 令牌查询
GetUUIDByAccessToken(ctx context.Context, accessToken string) (string, error)
GetUserIDByAccessToken(ctx context.Context, accessToken string) (int64, error)
}
// VerificationService 验证码服务接口
type VerificationService interface {
SendCode(ctx context.Context, email, codeType string) error
VerifyCode(ctx context.Context, email, code, codeType string) error
}
// CaptchaService 滑动验证码服务接口
type CaptchaService interface {
Generate(ctx context.Context) (masterImg, tileImg, captchaID string, y int, err error)
Verify(ctx context.Context, dx int, captchaID string) (bool, error)
CheckVerified(ctx context.Context, captchaID string) (bool, error)
ConsumeVerified(ctx context.Context, captchaID string) error
}
// YggdrasilService Yggdrasil服务接口
type YggdrasilService interface {
// 用户认证
GetUserIDByEmail(ctx context.Context, email string) (int64, error)
VerifyPassword(ctx context.Context, password string, userID int64) error
// 会话管理
JoinServer(ctx context.Context, serverID, accessToken, selectedProfile, ip string) error
HasJoinedServer(ctx context.Context, serverID, username, ip string) error
// 密码管理
ResetYggdrasilPassword(ctx context.Context, userID int64) (string, error)
// 序列化
SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{}
SerializeProfileWithUnsigned(ctx context.Context, profile model.Profile, unsigned bool) map[string]interface{}
SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{}
// 证书
GeneratePlayerCertificate(ctx context.Context, uuid string) (*PlayerCertificate, error)
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 {
// 登录安全
CheckLoginLocked(ctx context.Context, identifier string) (bool, time.Duration, error)
RecordLoginFailure(ctx context.Context, identifier string) (int, error)
ClearLoginAttempts(ctx context.Context, identifier string) error
GetRemainingLoginAttempts(ctx context.Context, identifier string) (int, error)
// 验证码安全
CheckVerifyLocked(ctx context.Context, email, codeType string) (bool, time.Duration, error)
RecordVerifyFailure(ctx context.Context, email, codeType string) (int, error)
ClearVerifyAttempts(ctx context.Context, email, codeType string) error
}
// Services 服务集合
type Services struct {
User UserService
Profile ProfileService
Texture TextureService
Token TokenService
Verification VerificationService
Captcha CaptchaService
Yggdrasil YggdrasilService
Security SecurityService
Friends FriendsService
}
// ServiceDeps 服务依赖
type ServiceDeps struct {
Logger *zap.Logger
Storage *storage.StorageClient
}