Files
backend/internal/errors/errors.go
lan f2b02682c2
Some checks failed
Build / build-docker (push) Has been cancelled
Build / build (push) Has been cancelled
feat(yggdrasil): 实现好友系统与 profiles 查询接口
按 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 ./... 全部通过
2026-07-09 20:44:41 +08:00

187 lines
6.4 KiB
Go
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.
// 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,
}
}