Files
backend/internal/repository/profile_repository.go

187 lines
5.6 KiB
Go
Raw Normal View History

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
2025-12-03 10:58:39 +08:00
// 使用 LOWER 函数进行不区分大小写的查询,并预加载 Skin 和 Cape
err := r.db.WithContext(ctx).Where("LOWER(name) = LOWER(?)", name).
2025-12-03 10:58:39 +08:00
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) {
var profiles []*model.Profile
err := r.db.WithContext(ctx).Where("name in (?)", names).
2025-12-03 10:58:39 +08:00
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).
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
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)
}
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
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).
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
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
})
}