Files
backend/internal/container/container.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

154 lines
5.2 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 container
import (
"context"
"time"
"carrotskin/internal/repository"
"carrotskin/internal/service"
"carrotskin/pkg/auth"
"carrotskin/pkg/config"
"carrotskin/pkg/database"
"carrotskin/pkg/email"
"carrotskin/pkg/redis"
"carrotskin/pkg/storage"
"go.uber.org/zap"
"gorm.io/gorm"
)
// Container 依赖注入容器
// 集中管理所有依赖,便于测试和维护
type Container struct {
// 配置
Config *config.Config
// 基础设施依赖
DB *gorm.DB
Redis *redis.Client
Logger *zap.Logger
JWT *auth.JWTService
Casbin *auth.CasbinService
Storage *storage.StorageClient
CacheManager *database.CacheManager
// Repository层
UserRepo repository.UserRepository
ProfileRepo repository.ProfileRepository
TextureRepo repository.TextureRepository
ClientRepo repository.ClientRepository
YggdrasilRepo repository.YggdrasilRepository
FriendRepo repository.FriendRepository
PlayerAttrRepo repository.PlayerAttributeRepository
// Service层
UserService service.UserService
ProfileService service.ProfileService
TextureService service.TextureService
TokenService service.TokenService
YggdrasilService service.YggdrasilService
FriendsService service.FriendsService
VerificationService service.VerificationService
CaptchaService service.CaptchaService
SignatureService *service.SignatureService
}
// NewContainer 创建依赖容器
func NewContainer(
cfg *config.Config,
db *gorm.DB,
redisClient *redis.Client,
logger *zap.Logger,
jwtService *auth.JWTService,
casbinService *auth.CasbinService,
storageClient *storage.StorageClient,
emailService *email.Service,
) *Container {
// 创建缓存管理器
cacheManager := database.NewCacheManager(redisClient, database.CacheConfig{
Prefix: "carrotskin:",
Expiration: 5 * time.Minute,
Enabled: true,
Policy: database.CachePolicy{
UserTTL: 5 * time.Minute,
UserEmailTTL: 5 * time.Minute,
ProfileTTL: 5 * time.Minute,
ProfileListTTL: 3 * time.Minute,
TextureTTL: 5 * time.Minute,
TextureListTTL: 2 * time.Minute,
},
})
c := &Container{
Config: cfg,
DB: db,
Redis: redisClient,
Logger: logger,
JWT: jwtService,
Casbin: casbinService,
Storage: storageClient,
CacheManager: cacheManager,
}
// 初始化Repository
c.UserRepo = repository.NewUserRepository(db)
c.ProfileRepo = repository.NewProfileRepository(db)
c.TextureRepo = repository.NewTextureRepository(db)
c.ClientRepo = repository.NewClientRepository(db)
c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
c.FriendRepo = repository.NewFriendRepository(db)
c.PlayerAttrRepo = repository.NewPlayerAttributeRepository(db)
// 初始化SignatureService作为依赖注入避免在容器中创建并立即调用
// 将SignatureService添加到容器中供其他服务使用
c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger)
// 初始化Service注入缓存管理器
c.UserService = service.NewUserService(cfg, c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger, db)
// 获取Yggdrasil私钥并创建JWT服务TokenService需要
// 注意这里仍然需要预先初始化因为TokenService在创建时需要YggdrasilJWT
// 但SignatureService已经作为依赖注入降低了耦合度
// 使用后台 ctx启动阶段不依赖请求 ctx
initCtx, cancelInit := context.WithTimeout(context.Background(), 30*time.Second)
_, privateKey, err := c.SignatureService.GetOrCreateYggdrasilKeyPair(initCtx)
cancelInit()
if err != nil {
logger.Fatal("获取Yggdrasil私钥失败", zap.Error(err))
}
yggdrasilJWT := auth.NewYggdrasilJWTService(privateKey, "carrotskin")
// 创建Redis Token存储必须使用Redis包括miniredis回退
if redisClient == nil {
logger.Fatal("Redis客户端未初始化无法创建Token服务")
}
tokenStore := auth.NewTokenStoreRedis(
redisClient,
logger,
auth.WithKeyPrefix("token:"),
auth.WithDefaultTTL(24*time.Hour),
auth.WithStaleTTL(30*24*time.Hour),
auth.WithMaxTokensPerUser(10),
)
c.TokenService = service.NewTokenServiceRedis(tokenStore, c.ClientRepo, c.ProfileRepo, yggdrasilJWT, logger)
// 使用组合服务(内部包含认证、会话、序列化、证书服务)
c.YggdrasilService = service.NewYggdrasilServiceComposite(c.UserRepo, c.ProfileRepo, c.YggdrasilRepo, c.TextureRepo, c.SignatureService, redisClient, logger, c.TokenService)
// 好友系统服务(依赖 Profile/Redis
c.FriendsService = service.NewFriendsService(c.FriendRepo, c.PlayerAttrRepo, c.ProfileRepo, redisClient, logger)
// 初始化其他服务
c.CaptchaService = service.NewCaptchaService(cfg, redisClient, logger)
// 初始化VerificationService需要email.Service
if emailService != nil {
c.VerificationService = service.NewVerificationService(cfg, redisClient, emailService)
}
return c
}