按 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 ./... 全部通过
192 lines
5.8 KiB
Go
192 lines
5.8 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"carrotskin/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// profileRepository ProfileRepository的实现
|
|
type profileRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewProfileRepository 创建ProfileRepository实例
|
|
func NewProfileRepository(db *gorm.DB) ProfileRepository {
|
|
return &profileRepository{db: db}
|
|
}
|
|
|
|
func (r *profileRepository) Create(ctx context.Context, profile *model.Profile) error {
|
|
return r.db.WithContext(ctx).Create(profile).Error
|
|
}
|
|
|
|
func (r *profileRepository) FindByUUID(ctx context.Context, uuid string) (*model.Profile, error) {
|
|
var profile model.Profile
|
|
err := r.db.WithContext(ctx).Where("uuid = ?", uuid).
|
|
Preload("Skin").
|
|
Preload("Cape").
|
|
First(&profile).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &profile, nil
|
|
}
|
|
|
|
func (r *profileRepository) FindByName(ctx context.Context, name string) (*model.Profile, error) {
|
|
var profile model.Profile
|
|
// 使用 LOWER 函数进行不区分大小写的查询,并预加载 Skin 和 Cape
|
|
err := r.db.WithContext(ctx).Where("LOWER(name) = LOWER(?)", name).
|
|
Preload("Skin").
|
|
Preload("Cape").
|
|
First(&profile).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &profile, nil
|
|
}
|
|
|
|
func (r *profileRepository) FindByUserID(ctx context.Context, userID int64) ([]*model.Profile, error) {
|
|
var profiles []*model.Profile
|
|
err := r.db.WithContext(ctx).Where("user_id = ?", userID).
|
|
Preload("Skin").
|
|
Preload("Cape").
|
|
Order("created_at DESC").
|
|
Find(&profiles).Error
|
|
return profiles, err
|
|
}
|
|
|
|
func (r *profileRepository) FindByUUIDs(ctx context.Context, uuids []string) ([]*model.Profile, error) {
|
|
if len(uuids) == 0 {
|
|
return []*model.Profile{}, nil
|
|
}
|
|
var profiles []*model.Profile
|
|
// 使用 IN 查询优化批量查询,并预加载关联
|
|
err := r.db.WithContext(ctx).Where("uuid IN ?", uuids).
|
|
Preload("Skin").
|
|
Preload("Cape").
|
|
Find(&profiles).Error
|
|
return profiles, err
|
|
}
|
|
|
|
func (r *profileRepository) Update(ctx context.Context, profile *model.Profile) error {
|
|
return r.db.WithContext(ctx).Save(profile).Error
|
|
}
|
|
|
|
func (r *profileRepository) UpdateFields(ctx context.Context, uuid string, updates map[string]interface{}) error {
|
|
return r.db.WithContext(ctx).Model(&model.Profile{}).
|
|
Where("uuid = ?", uuid).
|
|
Updates(updates).Error
|
|
}
|
|
|
|
func (r *profileRepository) Delete(ctx context.Context, uuid string) error {
|
|
return r.db.WithContext(ctx).Where("uuid = ?", uuid).Delete(&model.Profile{}).Error
|
|
}
|
|
|
|
func (r *profileRepository) BatchUpdate(ctx context.Context, uuids []string, updates map[string]interface{}) (int64, error) {
|
|
if len(uuids) == 0 {
|
|
return 0, nil
|
|
}
|
|
result := r.db.WithContext(ctx).Model(&model.Profile{}).Where("uuid IN ?", uuids).Updates(updates)
|
|
return result.RowsAffected, result.Error
|
|
}
|
|
|
|
func (r *profileRepository) BatchDelete(ctx context.Context, uuids []string) (int64, error) {
|
|
if len(uuids) == 0 {
|
|
return 0, nil
|
|
}
|
|
result := r.db.WithContext(ctx).Where("uuid IN ?", uuids).Delete(&model.Profile{})
|
|
return result.RowsAffected, result.Error
|
|
}
|
|
|
|
func (r *profileRepository) CountByUserID(ctx context.Context, userID int64) (int64, error) {
|
|
var count int64
|
|
err := r.db.WithContext(ctx).Model(&model.Profile{}).
|
|
Where("user_id = ?", userID).
|
|
Count(&count).Error
|
|
return count, err
|
|
}
|
|
|
|
func (r *profileRepository) UpdateLastUsedAt(ctx context.Context, uuid string) error {
|
|
return r.db.WithContext(ctx).Model(&model.Profile{}).
|
|
Where("uuid = ?", uuid).
|
|
Update("last_used_at", gorm.Expr("CURRENT_TIMESTAMP")).Error
|
|
}
|
|
|
|
func (r *profileRepository) GetByNames(ctx context.Context, names []string) ([]*model.Profile, error) {
|
|
if len(names) == 0 {
|
|
return []*model.Profile{}, nil
|
|
}
|
|
var profiles []*model.Profile
|
|
// 与 FindByName 保持一致:使用 LOWER() 做大小写不敏感匹配,
|
|
// 客户端按文档会 toLowerCase 规范化用户名后发送。
|
|
err := r.db.WithContext(ctx).Where("LOWER(name) IN (?)", names).
|
|
Preload("Skin").
|
|
Preload("Cape").
|
|
Find(&profiles).Error
|
|
return profiles, err
|
|
}
|
|
|
|
func (r *profileRepository) GetKeyPair(ctx context.Context, profileId string) (*model.KeyPair, error) {
|
|
if profileId == "" {
|
|
return nil, errors.New("参数不能为空")
|
|
}
|
|
|
|
var profile model.Profile
|
|
result := r.db.WithContext(ctx).
|
|
Select("rsa_private_key", "rsa_public_key", "public_key_signature", "public_key_signature_v2", "key_expires_at", "key_refresh_at").
|
|
Where("uuid = ?", profileId).
|
|
First(&profile)
|
|
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("key pair未找到")
|
|
}
|
|
return nil, fmt.Errorf("获取key pair失败: %w", result.Error)
|
|
}
|
|
|
|
keyPair := &model.KeyPair{
|
|
PrivateKey: profile.RSAPrivateKey,
|
|
PublicKey: profile.RSAPublicKey,
|
|
PublicKeySignature: profile.PublicKeySignature,
|
|
PublicKeySignatureV2: profile.PublicKeySignatureV2,
|
|
}
|
|
if profile.KeyExpiresAt != nil {
|
|
keyPair.Expiration = *profile.KeyExpiresAt
|
|
}
|
|
if profile.KeyRefreshAt != nil {
|
|
keyPair.Refresh = *profile.KeyRefreshAt
|
|
}
|
|
return keyPair, nil
|
|
}
|
|
|
|
func (r *profileRepository) UpdateKeyPair(ctx context.Context, profileId string, keyPair *model.KeyPair) error {
|
|
if profileId == "" {
|
|
return errors.New("profileId 不能为空")
|
|
}
|
|
if keyPair == nil {
|
|
return errors.New("keyPair 不能为 nil")
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
result := tx.Model(&model.Profile{}).
|
|
Where("uuid = ?", profileId).
|
|
Updates(map[string]interface{}{
|
|
"rsa_private_key": keyPair.PrivateKey,
|
|
"rsa_public_key": keyPair.PublicKey,
|
|
"public_key_signature": keyPair.PublicKeySignature,
|
|
"public_key_signature_v2": keyPair.PublicKeySignatureV2,
|
|
"key_expires_at": keyPair.Expiration,
|
|
"key_refresh_at": keyPair.Refresh,
|
|
})
|
|
|
|
if result.Error != nil {
|
|
return fmt.Errorf("更新 keyPair 失败: %w", result.Error)
|
|
}
|
|
return nil
|
|
})
|
|
}
|