按 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 ./... 全部通过
187 lines
6.4 KiB
Go
187 lines
6.4 KiB
Go
// Package errors 定义应用程序的错误类型
|
||
package errors
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
)
|
||
|
||
// 预定义错误
|
||
var (
|
||
// 用户相关错误
|
||
ErrUserNotFound = errors.New("用户不存在")
|
||
ErrUserAlreadyExists = errors.New("用户已存在")
|
||
ErrEmailAlreadyExists = errors.New("邮箱已被注册")
|
||
ErrAccountDisabled = errors.New("账号已被禁用")
|
||
|
||
// 认证相关错误
|
||
ErrUnauthorized = errors.New("未授权")
|
||
ErrInvalidToken = errors.New("无效的令牌")
|
||
ErrTokenExpired = errors.New("令牌已过期")
|
||
|
||
// 档案相关错误
|
||
ErrProfileNotFound = errors.New("档案不存在")
|
||
ErrProfileNameExists = errors.New("角色名已被使用")
|
||
ErrProfileLimitReached = errors.New("已达档案数量上限")
|
||
ErrProfileNoPermission = errors.New("无权操作此档案")
|
||
|
||
// 材质相关错误
|
||
ErrTextureNotFound = errors.New("材质不存在")
|
||
ErrTextureExists = errors.New("该材质已存在")
|
||
ErrTextureLimitReached = errors.New("已达材质数量上限")
|
||
ErrTextureNoPermission = errors.New("无权操作此材质")
|
||
|
||
// 验证码相关错误
|
||
ErrInvalidVerificationCode = errors.New("验证码错误或已过期")
|
||
ErrTooManyAttempts = errors.New("尝试次数过多")
|
||
ErrSendTooFrequent = errors.New("发送过于频繁")
|
||
|
||
// URL验证相关错误
|
||
ErrInvalidURL = errors.New("无效的URL格式")
|
||
ErrDomainNotAllowed = errors.New("URL域名不在允许的列表中")
|
||
|
||
// 存储相关错误
|
||
ErrStorageUnavailable = errors.New("存储服务不可用")
|
||
ErrUploadFailed = errors.New("上传失败")
|
||
|
||
// Yggdrasil相关错误
|
||
ErrPasswordMismatch = errors.New("密码错误")
|
||
ErrPasswordNotSet = errors.New("未生成密码")
|
||
ErrInvalidServerID = errors.New("服务器ID格式无效")
|
||
ErrSessionNotFound = errors.New("会话不存在或已过期")
|
||
ErrSessionMismatch = errors.New("会话验证失败")
|
||
ErrUsernameMismatch = errors.New("用户名不匹配")
|
||
ErrUsernameRequired = errors.New("用户名不能为空")
|
||
ErrIPMismatch = errors.New("IP地址不匹配")
|
||
ErrInvalidAccessToken = errors.New("访问令牌无效")
|
||
ErrProfileMismatch = errors.New("selectedProfile与Token不匹配")
|
||
ErrUUIDRequired = errors.New("UUID不能为空")
|
||
|
||
// 通用错误
|
||
ErrBadRequest = errors.New("请求参数错误")
|
||
ErrInternalServer = errors.New("服务器内部错误")
|
||
ErrNotFound = errors.New("资源不存在")
|
||
ErrForbidden = errors.New("权限不足")
|
||
)
|
||
|
||
// AppError 应用错误类型,包含错误码和消息
|
||
type AppError struct {
|
||
Code int // HTTP状态码
|
||
Message string // 用户可见的错误消息
|
||
Err error // 原始错误(用于日志)
|
||
}
|
||
|
||
// Error 实现error接口
|
||
func (e *AppError) Error() string {
|
||
if e.Err != nil {
|
||
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||
}
|
||
return e.Message
|
||
}
|
||
|
||
// Unwrap 支持errors.Is和errors.As
|
||
func (e *AppError) Unwrap() error {
|
||
return e.Err
|
||
}
|
||
|
||
// NewAppError 创建新的应用错误
|
||
func NewAppError(code int, message string, err error) *AppError {
|
||
return &AppError{
|
||
Code: code,
|
||
Message: message,
|
||
Err: err,
|
||
}
|
||
}
|
||
|
||
// NewBadRequest 创建400错误
|
||
func NewBadRequest(message string, err error) *AppError {
|
||
return NewAppError(400, message, err)
|
||
}
|
||
|
||
// NewUnauthorized 创建401错误
|
||
func NewUnauthorized(message string) *AppError {
|
||
return NewAppError(401, message, nil)
|
||
}
|
||
|
||
// NewForbidden 创建403错误
|
||
func NewForbidden(message string) *AppError {
|
||
return NewAppError(403, message, nil)
|
||
}
|
||
|
||
// NewNotFound 创建404错误
|
||
func NewNotFound(message string) *AppError {
|
||
return NewAppError(404, message, nil)
|
||
}
|
||
|
||
// NewInternalError 创建500错误
|
||
func NewInternalError(message string, err error) *AppError {
|
||
return NewAppError(500, message, err)
|
||
}
|
||
|
||
// 注意:原此处的 Is/As/Wrap 函数(透传标准库)已删除。
|
||
// 请直接使用标准库 errors.Is / errors.As / fmt.Errorf。
|
||
|
||
// YggdrasilErrorResponse Yggdrasil协议标准错误响应格式
|
||
type YggdrasilErrorResponse struct {
|
||
Error string `json:"error"` // 错误的简要描述(机器可读)
|
||
ErrorMessage string `json:"errorMessage"` // 错误的详细信息(人类可读)
|
||
Cause string `json:"cause,omitempty"` // 该错误的原因(可选)
|
||
}
|
||
|
||
// NewYggdrasilErrorResponse 创建Yggdrasil标准错误响应
|
||
func NewYggdrasilErrorResponse(error, errorMessage, cause string) *YggdrasilErrorResponse {
|
||
return &YggdrasilErrorResponse{
|
||
Error: error,
|
||
ErrorMessage: errorMessage,
|
||
Cause: cause,
|
||
}
|
||
}
|
||
|
||
// YggdrasilErrorCodes Yggdrasil协议错误码常量
|
||
const (
|
||
// ForbiddenOperationException 错误消息
|
||
YggErrInvalidToken = "Invalid token."
|
||
YggErrInvalidCredentials = "Invalid credentials. Invalid username or password."
|
||
|
||
// IllegalArgumentException 错误消息
|
||
YggErrProfileAlreadyAssigned = "Access token already has a profile assigned."
|
||
)
|
||
|
||
// FriendsErrorStatus MinecraftServices 好友系接口业务错误码(文档 1.3.4)
|
||
const (
|
||
FriendsErrUnknownProfile = "UNKNOWN_PROFILE"
|
||
FriendsErrCannotAddSelf = "CANNOT_ADD_SELF"
|
||
FriendsErrDuplicatedProfiles = "DUPLICATED_PROFILES"
|
||
)
|
||
|
||
// FriendsErrorResultCode MinecraftServices 好友系接口结果码(文档 4)
|
||
const (
|
||
FriendsResultSuccess = "SUCCESS"
|
||
FriendsResultError = "ERROR"
|
||
FriendsResultServiceUnavailable = "SERVICE_NOT_AVAILABLE"
|
||
FriendsResultTooManyRequests = "TOO_MANY_REQUESTS"
|
||
FriendsResultForbidden = "FORBIDDEN"
|
||
FriendsResultUnknownProfile = "UNKNOWN_PROFILE"
|
||
FriendsResultUnauthorized = "UNAUTHORIZED"
|
||
)
|
||
|
||
// MinecraftServicesErrorResponse MinecraftServices 系接口标准错误响应(文档 0.3)
|
||
// 与 YggdrasilErrorResponse 不同,本体系按 MinecraftServices 的结构返回:
|
||
// { "path": "...", "error": "...", "errorMessage": "...", "details": {} }
|
||
type MinecraftServicesErrorResponse struct {
|
||
Path string `json:"path"`
|
||
Error string `json:"error"` // 机器可读错误码
|
||
ErrorMessage string `json:"errorMessage"` // 人类可读描述
|
||
Details interface{} `json:"details,omitempty"` // 错误特定细节
|
||
}
|
||
|
||
// NewMinecraftServicesErrorResponse 创建 MinecraftServices 系标准错误响应
|
||
func NewMinecraftServicesErrorResponse(path, errorCode, errorMessage string, details interface{}) *MinecraftServicesErrorResponse {
|
||
return &MinecraftServicesErrorResponse{
|
||
Path: path,
|
||
Error: errorCode,
|
||
ErrorMessage: errorMessage,
|
||
Details: details,
|
||
}
|
||
}
|