Files
backend/internal/service/interfaces.go

290 lines
11 KiB
Go
Raw Normal View History

// 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)
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
// 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)
2025-12-03 10:58:39 +08:00
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{}
// 证书
refactor(yggdrasil): 整理与性能优化,修复若干 Bug Bug 修复 - textures metadata:SKIN 仅在 IsSlim 时输出 {"model":"slim"};CAPE 不带 metadata。 原实现误用文件字节数 skin.Size 作为 metadata,违反 Yggdrasil 协议。 - KeyPair 持久化不全:Profile 新增 rsa_public_key/public_key_signature/ public_key_signature_v2/key_expires_at/key_refresh_at 列;GetKeyPair/UpdateKeyPair 读写全部字段。AutoMigrate 自动加列。原实现每次请求都重新生成 RSA-4096。 - GetPlayerCertificates:无效 token 返回 401(先前误用 403 且未判 err)。 - HasJoinedServer:失败返回 204 无 body,符合 Yggdrasil 协议(原用 APIResponse + body 违反 204 规范)。 代码质量 - Authenticate 删除冗余 body 读取与回放。 - 证书服务引入具名结构 PlayerCertificate/PlayerKeyPair,替代 map[string]interface{}。 - CreateSession 用户名缺失改用 ErrUsernameRequired(新增)。 性能优化(签名一致性的回归测试已覆盖) - SignatureService 缓存已解析的根私钥(sync.RWMutex 双重检查), 快路径仅一次 RLock + RSA 签名,免去每次签名 PEM 解析与 Redis 往返。 - GetOrCreateYggdrasilKeyPair 用 MGet 单次往返取三字段,缓存命中后零 Redis。 - NewKeyPair 消息构造用 strconv.AppendInt 避免 string+拼接分配;V2 复用 DER 字节。 - 序列化服务:texturesMap 预分配 cap(2);slim 分支直接构造完整 map。 - 会话服务 GetSession 移除重复的 ValidateServerID 校验。 死代码清理 - 删除 yggdrasil_validator.go(未被调用的 Validator)。 - 删除 signature_service.FormatPublicKey/SignStringWithProfileRSA。 - 删除 pkg/auth 中未被使用的 YggdrasilJWTManager 与 GenerateKeyPair/ EncodePrivateKeyToPEM/RedisClient/YggdrasilPrivateKeyRedisKey。 - 删除 yggdrasil_handler.go 中 25 个未使用常量、passwordRegex、 APIResponse/standardResponse。 - 删除 errors.go 中未被使用的 ErrYggForbiddenOperation/ErrYggIllegalArgument/ ErrInvalidSignature/ErrInvalidTextureType/ErrCertificateGenerate/ErrInvalidPassword。 测试 - 新增 signature_service_test.go(基于 miniredis):8 个测试 + 基准。 核心:SignString_MatchesReference 与重构前参考实现逐字节比对签名输出一致。 -race 检测通过;基准约 7.1ms/op(缓存命中路径)。 附带(与本次任务前已存在的未提交改动) - handler/captcha_handler:将响应字段 msg 改为 message,与前端约定对齐。
2026-07-09 17:18:26 +08:00
GeneratePlayerCertificate(ctx context.Context, uuid string) (*PlayerCertificate, error)
GetPublicKey(ctx context.Context) (string, error)
}
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
// 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
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
Friends FriendsService
}
// ServiceDeps 服务依赖
type ServiceDeps struct {
Logger *zap.Logger
Storage *storage.StorageClient
}