Files
backend/plans/unsigned_uuid_design.md
lan 29f0bad2bc
All checks were successful
Build / build (push) Successful in 2m17s
Build / build-docker (push) Successful in 57s
feat(yggdrasil): implement standard error responses and UUID format improvements
- Add YggdrasilErrorResponse struct and standard error codes for protocol compliance
- Change UUID storage from varchar(36) to varchar(32) for unsigned format
- Add utility functions: GenerateUUID, FormatUUIDToNoDash, RandomHex
- Support unsigned query parameter in GetProfileByUUID endpoint
- Improve refresh token response with available profiles list
- Fix key pair retrieval to use correct database column (rsa_private_key)
- Update UUID validator to accept both 32-char and 36-char formats
- Add SignStringWithProfileRSA method for profile-specific signing
- Fix profile assignment validation in refresh token flow
2026-02-23 13:26:53 +08:00

10 KiB
Raw Blame History

无符号UUID实现方案设计

1. 概述

1.1 背景

当前项目中UUID使用带连字符的标准格式36位xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。为了与Yggdrasil协议更好的兼容并优化存储空间需要实现无符号UUID32位十六进制字符串

1.2 目标

  • 定义无符号UUID的格式规范
  • 设计UUID生成、保存、读取、返回的转换逻辑
  • 确保与Yggdrasil协议的兼容性
  • 提供向后兼容和数据迁移策略

1.3 无符号UUID格式

  • 格式: 32位十六进制字符串小写
  • 示例: 123e4567e89b12d3a456426614174000
  • 数据库存储: varchar(32)

2. 当前UUID使用分析

2.1 UUID使用场景

场景 文件位置 用途
Profile创建 internal/service/profile_service.go:67 生成Profile UUID
Client创建 internal/service/token_service_redis.go:76 生成Client UUID
数据库存储 internal/model/profile.go:10 Profile表主键 varchar(36)
数据库存储 internal/model/client.go:8 Client表主键 varchar(36)
API返回 internal/model/profile.go:34 ProfileResponse.JSON
Yggdrasil序列化 internal/service/yggdrasil_serialization_service.go:54 profileId字段
Yggdrasil序列化 internal/service/yggdrasil_serialization_service.go:113 id字段

2.2 现有转换函数

项目已在 pkg/utils/format.go 中实现了 FormatUUID 函数:

  • 功能32位字符串 → 带连字符的36位标准格式
  • 使用场景Handler层接收参数后转换为标准格式

3. 转换逻辑设计

3.1 核心转换函数

pkg/utils/format.go 中扩展以下函数:

// FormatUUIDToNoDash 将带连字符的UUID转换为无符号UUID移除连字符
// 输入: "123e4567-e89b-12d3-a456-426614174000"
// 输出: "123e4567e89b12d3a456426614174000"
func FormatUUIDToNoDash(uuid string) string

// FormatUUIDToDash 将无符号UUID转换为带连字符的标准格式
// 输入: "123e4567e89b12d3a456426614174000"
// 输出: "123e4567-e89b-12d3-a456-426614174000"
func FormatUUIDToDash(uuid string) string

3.2 UUID生成策略

// GenerateUUID 生成无符号UUID
// 使用github.com/google/uuid库生成标准UUID然后移除连字符
func GenerateUUID() string {
    return uuid.New().String() // 默认生成带连字符的UUID
    // 移除连字符
    return strings.ReplaceAll(uuid.New().String(), "-", "")
}

4. 数据流转设计

4.1 总体数据流

生成UUID (36位标准格式)
        ↓
移除连字符 (32位无符号UUID)
        ↓
存储到数据库 (varchar(32))
        ↓
读取时根据场景转换
        ├─→ Yggdrasil API: 保持无符号格式
        └─→ 内部处理/调试: 转换为标准格式

4.2 各环节处理

环节 操作 说明
生成 uuid.New().String() → 移除- 保持与google/uuid库兼容
保存 直接存储32位字符串 数据库字段改为varchar(32)
读取 直接读取 无需转换
返回 根据API类型选择 见4.3节

4.3 API返回策略

API类型 返回格式 原因
Yggdrasil协议 32位无符号 Mojang/Yggdrasil标准
内部REST API 保持存储格式 统一简洁
调试/日志 可选转换 便于阅读

5. 需要修改的文件

5.1 核心文件修改

文件 修改内容
pkg/utils/format.go 添加 FormatUUIDToNoDashFormatUUIDToDashGenerateUUID 函数
pkg/utils/format_test.go 添加新函数的单元测试
internal/model/profile.go UUID字段类型改为varchar(32)
internal/model/client.go UUID字段类型改为varchar(32)

5.2 服务层修改

文件 修改内容
internal/service/profile_service.go UUID生成改用新函数
internal/service/token_service_redis.go UUID生成改用新函数

5.3 Handler层修改

文件 修改内容
internal/handler/yggdrasil_handler.go Yggdrasil API返回无符号UUID
internal/handler/profile_handler.go 内部API返回无符号UUID

5.4 序列化服务修改

文件 修改内容
internal/service/yggdrasil_serialization_service.go profileId和id字段使用无符号UUID

6. 实现步骤

6.1 第一步:扩展工具函数

pkg/utils/format.go 中添加:

  1. FormatUUIDToNoDash(uuid string) string - 移除连字符
  2. FormatUUIDToDash(uuid string) string - 添加连字符(现有函数增强)
  3. GenerateUUID() string - 生成无符号UUID

6.2 第二步:修改数据模型

  1. 修改 internal/model/profile.go:

    UUID string `gorm:"column:uuid;type:varchar(32);primaryKey" json:"uuid"`
    
  2. 修改 internal/model/client.go:

    UUID string `gorm:"column:uuid;type:varchar(32);primaryKey" json:"uuid"`
    

6.3 第三步:修改服务层

  1. 修改 internal/service/profile_service.go:

    // 生成无符号UUID
    profileUUID := utils.GenerateUUID() // 或 uuid.New().String() 后转换
    
  2. 修改 internal/service/token_service_redis.go:

    clientUUID := utils.GenerateUUID()
    

6.4 第四步修改Handler层

  1. 修改 internal/handler/yggdrasil_handler.go:

    • 移除 utils.FormatUUID() 调用(因为存储的已经是无符号格式)
    • Yggdrasil API直接返回无符号UUID
  2. 修改 internal/handler/profile_handler.go:

    • 同样移除 utils.FormatUUID() 调用

6.第五步:修改序列化服务

修改 internal/service/yggdrasil_serialization_service.go:

// SerializeProfile - profileId使用无符号UUID
"profileId": profile.UUID,  // 直接使用存储的32位UUID

// SerializeProfile - id使用无符号UUID  
"id": profile.UUID,  // 直接使用存储的32位UUID

7. 向后兼容性设计

7.1 双格式支持(可选)

如果需要同时支持新旧客户端,可以:

  1. 数据库字段保持varchar(36) - 存储带连字符的格式
  2. 新增转换函数 - 在返回Yggdrasil响应时转换为无符号格式

7.2 推荐策略:渐进式迁移

  1. 第一阶段: 实现新代码生成和存储无符号UUID
  2. 第二阶段: 运行数据迁移脚本
  3. 第三阶段: 移除旧格式支持

8. 数据迁移策略

8.1 迁移脚本设计

-- 将profiles表的UUID转换为无符号格式
UPDATE profiles 
SET uuid = REPLACE(REPLACE(REPLACE(REPLACE(uuid, '-', ''), 
    SUBSTRING(uuid, 9, 1), ''), '', ''), '', '')
WHERE LENGTH(uuid) = 36;

-- 实际迁移SQL分步骤
UPDATE profiles 
SET uuid = CONCAT(
    SUBSTRING(uuid, 1, 8),
    SUBSTRING(uuid, 10, 4),
    SUBSTRING(uuid, 15, 4),
    SUBSTRING(uuid, 20, 4),
    SUBSTRING(uuid, 25, 12)
)
WHERE LENGTH(uuid) = 36 AND uuid LIKE '%-%-%-%-%';

-- 同样迁移clients表
UPDATE clients 
SET uuid = CONCAT(
    SUBSTRING(uuid, 1, 8),
    SUBSTRING(uuid, 10, 4),
    SUBSTRING(uuid, 15, 4),
    SUBSTRING(uuid, 20, 4),
    SUBSTRING(uuid, 25, 12)
)
WHERE LENGTH(uuid) = 36 AND uuid LIKE '%-%-%-%-%';

8.2 迁移注意事项

  1. 备份数据 - 迁移前务必备份数据库
  2. 停服迁移 - 建议在低峰期执行
  3. 验证迁移 - 迁移后验证数据完整性
  4. 回滚方案 - 准备回滚脚本

9. 测试计划

9.1 单元测试

pkg/utils/format_test.go 中添加:

  1. TestFormatUUIDToNoDash - 测试移除连字符
  2. TestFormatUUIDToDash - 测试添加连字符
  3. TestGenerateUUID - 测试UUID生成32位、唯一性

9.2 集成测试

  1. 创建Profile后验证数据库存储格式
  2. Yggdrasil认证流程验证UUID格式
  3. 批量操作BatchUpdate/BatchDelete验证UUID处理

10. 注意事项

10.1 Yggdrasil协议兼容性

根据Yggdrasil/Mojang协议规范

  • 玩家UUID应为32位无符号十六进制字符串
  • profileId字段应为小写
  • 与Minecraft客户端交互时使用此格式

10.2 性能考虑

  • 新UUID生成无需额外转换直接生成32位
  • 数据库varchar(32)比varchar(36)节省约10%存储空间
  • 索引查询效率略有提升

10.3 安全性

  • UUID应使用加密安全的随机数生成器
  • github.com/google/uuid库默认使用crypto/rand安全可靠

11. Mermaid数据流图

flowchart TD
    A[uuid.New] --> B{需要格式}
    B -->|存储| C[FormatUUIDToNoDash]
    B -->|Yggdrasil返回| C
    B -->|内部API返回| D[直接使用]
    
    C --> E[数据库 varchar(32)]
    D --> E
    
    E --> F{读取场景}
    F -->|Yggdrasil API| G[保持无符号]
    F -->|调试/日志| H[FormatUUIDToDash]
    
    G --> I[响应客户端]
    H --> J[控制台输出]

12. 总结

本方案实现了以下目标:

  1. 格式统一定义: 32位无符号十六进制字符串
  2. 最小化修改: 保持核心逻辑不变,仅调整格式转换
  3. Yggdrasil兼容: 符合Mojang协议的UUID格式要求
  4. 平滑迁移: 支持渐进式数据迁移
  5. 测试覆盖: 完整的单元测试和集成测试计划

方案实施后项目将统一使用无符号UUID格式提升与Yggdrasil协议的兼容性并优化存储空间。