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

333 lines
10 KiB
Markdown
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.
# 无符号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`](internal/service/profile_service.go:67) | 生成Profile UUID |
| Client创建 | [`internal/service/token_service_redis.go:76`](internal/service/token_service_redis.go:76) | 生成Client UUID |
| 数据库存储 | [`internal/model/profile.go:10`](internal/model/profile.go:10) | Profile表主键 varchar(36) |
| 数据库存储 | [`internal/model/client.go:8`](internal/model/client.go:8) | Client表主键 varchar(36) |
| API返回 | [`internal/model/profile.go:34`](internal/model/profile.go:34) | ProfileResponse.JSON |
| Yggdrasil序列化 | [`internal/service/yggdrasil_serialization_service.go:54`](internal/service/yggdrasil_serialization_service.go:54) | profileId字段 |
| Yggdrasil序列化 | [`internal/service/yggdrasil_serialization_service.go:113`](internal/service/yggdrasil_serialization_service.go:113) | id字段 |
### 2.2 现有转换函数
项目已在 [`pkg/utils/format.go`](pkg/utils/format.go) 中实现了 `FormatUUID` 函数:
- 功能32位字符串 → 带连字符的36位标准格式
- 使用场景Handler层接收参数后转换为标准格式
---
## 3. 转换逻辑设计
### 3.1 核心转换函数
在 [`pkg/utils/format.go`](pkg/utils/format.go) 中扩展以下函数:
```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生成策略
```go
// 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`](pkg/utils/format.go) | 添加 `FormatUUIDToNoDash``FormatUUIDToDash``GenerateUUID` 函数 |
| [`pkg/utils/format_test.go`](pkg/utils/format_test.go) | 添加新函数的单元测试 |
| [`internal/model/profile.go`](internal/model/profile.go) | UUID字段类型改为varchar(32) |
| [`internal/model/client.go`](internal/model/client.go) | UUID字段类型改为varchar(32) |
### 5.2 服务层修改
| 文件 | 修改内容 |
|------|----------|
| [`internal/service/profile_service.go`](internal/service/profile_service.go) | UUID生成改用新函数 |
| [`internal/service/token_service_redis.go`](internal/service/token_service_redis.go) | UUID生成改用新函数 |
### 5.3 Handler层修改
| 文件 | 修改内容 |
|------|----------|
| [`internal/handler/yggdrasil_handler.go`](internal/handler/yggdrasil_handler.go) | Yggdrasil API返回无符号UUID |
| [`internal/handler/profile_handler.go`](internal/handler/profile_handler.go) | 内部API返回无符号UUID |
### 5.4 序列化服务修改
| 文件 | 修改内容 |
|------|----------|
| [`internal/service/yggdrasil_serialization_service.go`](internal/service/yggdrasil_serialization_service.go) | profileId和id字段使用无符号UUID |
---
## 6. 实现步骤
### 6.1 第一步:扩展工具函数
在 [`pkg/utils/format.go`](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`](internal/model/profile.go):
```go
UUID string `gorm:"column:uuid;type:varchar(32);primaryKey" json:"uuid"`
```
2. 修改 [`internal/model/client.go`](internal/model/client.go):
```go
UUID string `gorm:"column:uuid;type:varchar(32);primaryKey" json:"uuid"`
```
### 6.3 第三步:修改服务层
1. 修改 [`internal/service/profile_service.go`](internal/service/profile_service.go:67):
```go
// 生成无符号UUID
profileUUID := utils.GenerateUUID() // 或 uuid.New().String() 后转换
```
2. 修改 [`internal/service/token_service_redis.go`](internal/service/token_service_redis.go:76):
```go
clientUUID := utils.GenerateUUID()
```
### 6.4 第四步修改Handler层
1. 修改 [`internal/handler/yggdrasil_handler.go`](internal/handler/yggdrasil_handler.go):
- 移除 `utils.FormatUUID()` 调用(因为存储的已经是无符号格式)
- Yggdrasil API直接返回无符号UUID
2. 修改 [`internal/handler/profile_handler.go`](internal/handler/profile_handler.go):
- 同样移除 `utils.FormatUUID()` 调用
### 6.第五步:修改序列化服务
修改 [`internal/service/yggdrasil_serialization_service.go`](internal/service/yggdrasil_serialization_service.go):
```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 迁移脚本设计
```sql
-- 将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`](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数据流图
```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协议的兼容性并优化存储空间。