6 Commits

Author SHA1 Message Date
9e83ae16af 更新readme 2025-12-26 21:32:32 +08:00
85a9463913 解决合并后出现的问题,为swagger提供禁用选项,暂时移除wiki 2025-12-26 01:15:17 +08:00
44f007936e Merge remote-tracking branch 'origin/feature/redis-auth-integration' into dev
# Conflicts:
#	go.mod
#	go.sum
#	internal/container/container.go
#	internal/repository/interfaces.go
#	internal/service/mocks_test.go
#	internal/service/texture_service_test.go
#	internal/service/token_service_test.go
#	pkg/redis/manager.go
2025-12-25 22:45:58 +08:00
lan
6ddcf92ce3 refactor: Remove Token management and integrate Redis for authentication
- Deleted the Token model and its repository, transitioning to a Redis-based token management system.
- Updated the service layer to utilize Redis for token storage, enhancing performance and scalability.
- Refactored the container to remove TokenRepository and integrate the new token service.
- Cleaned up the Dockerfile and other files by removing unnecessary whitespace and comments.
- Enhanced error handling and logging for Redis initialization and usage.
2025-12-24 16:03:46 +08:00
9b0a60033e 删除服务端材质渲染功能及system_config表,转为环境变量配置,初步配置管理员功能 2025-12-08 19:12:30 +08:00
399e6f096f 暂存服务端渲染功能,材质渲染计划迁移至前端 2025-12-08 17:40:28 +08:00
133 changed files with 3501 additions and 22944 deletions

View File

@@ -73,10 +73,3 @@ scripts/
local/ local/
dev/ dev/
minio-data/ minio-data/

View File

@@ -2,11 +2,27 @@
# 复制此文件为 .env 后修改配置值 # 复制此文件为 .env 后修改配置值
# 此文件用于 docker-compose 部署,变量名与 docker-compose.yml 中的引用一致 # 此文件用于 docker-compose 部署,变量名与 docker-compose.yml 中的引用一致
# ==================== 站点配置 ====================
SITE_NAME=CarrotSkin
SITE_DESCRIPTION=一个优秀的Minecraft皮肤站
REGISTRATION_ENABLED=true
DEFAULT_AVATAR=
# ==================== 用户限制配置 ====================
MAX_TEXTURES_PER_USER=50
MAX_PROFILES_PER_USER=5
# ==================== 积分配置 ====================
CHECKIN_REWARD=10
TEXTURE_DOWNLOAD_REWARD=1
# ==================== 服务配置 ==================== # ==================== 服务配置 ====================
# 应用对外端口 # 应用对外端口
APP_PORT=8080 APP_PORT=8080
# 运行模式: debug, release, test # 运行模式: debug, release, test
SERVER_MODE=release SERVER_MODE=release
# 是否启用 Swagger 文档: true, false
SERVER_SWAGGER_ENABLED=true
# ==================== 数据库配置 ==================== # ==================== 数据库配置 ====================
# 数据库密码,生产环境务必修改 # 数据库密码,生产环境务必修改

View File

@@ -1,6 +1,26 @@
# CarrotSkin 环境配置文件示例 # CarrotSkin 环境配置文件示例
# 复制此文件为 .env 并修改相应的配置值 # 复制此文件为 .env 并修改相应的配置值
# =============================================================================
# 站点配置
# =============================================================================
SITE_NAME=CarrotSkin
SITE_DESCRIPTION=一个优秀的Minecraft皮肤站
REGISTRATION_ENABLED=true
DEFAULT_AVATAR=
# =============================================================================
# 用户限制配置
# =============================================================================
MAX_TEXTURES_PER_USER=50
MAX_PROFILES_PER_USER=5
# =============================================================================
# 积分配置
# =============================================================================
CHECKIN_REWARD=10
TEXTURE_DOWNLOAD_REWARD=1
# ============================================================================= # =============================================================================
# 服务器配置 # 服务器配置
# ============================================================================= # =============================================================================
@@ -8,6 +28,7 @@ SERVER_PORT=:8080
SERVER_MODE=debug SERVER_MODE=debug
SERVER_READ_TIMEOUT=30s SERVER_READ_TIMEOUT=30s
SERVER_WRITE_TIMEOUT=30s SERVER_WRITE_TIMEOUT=30s
SERVER_SWAGGER_ENABLED=true
# ============================================================================= # =============================================================================
# 数据库配置 # 数据库配置

1
.gitignore vendored
View File

@@ -109,3 +109,4 @@ dev/
service_coverage service_coverage
.gitignore .gitignore
docs/ docs/
blessing skin材质渲染示例/

View File

@@ -1,452 +0,0 @@
# API参考
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [swagger.go](file://internal/handler/swagger.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [user_handler.go](file://internal/handler/user_handler.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [captcha_handler.go](file://internal/handler/captcha_handler.go)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
- [auth.go](file://internal/middleware/auth.go)
- [response.go](file://internal/model/response.go)
- [common.go](file://internal/types/common.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本API参考面向CarrotSkin后端服务覆盖认证、用户、材质、档案、验证码、Yggdrasil与系统配置等API组。文档基于routes.go与swagger.go中的路由与注释结合各处理器文件的注释与类型定义提供
- 端点清单与HTTP方法、URL模式
- 请求/响应模式与鉴权要求JWT
- 错误码与常见错误场景
- 参数校验规则与示例
- 性能优化建议与最佳实践
## 项目结构
- 路由注册集中在路由文件,按版本分组与鉴权中间件组合
- Swagger文档与健康检查在统一入口启用
- 各功能模块处理器文件通过注释提供OpenAPI规范
- 中间件负责JWT鉴权与可选鉴权
- 响应模型与类型定义集中于model与types目录
```mermaid
graph TB
A["Gin引擎"] --> B["Swagger文档<br/>/swagger/*any"]
A --> C["健康检查<br/>/health"]
A --> D["API v1 组<br/>/api/v1"]
D --> E["认证组<br/>/api/v1/auth/*"]
D --> F["用户组<br/>/api/v1/user/*"]
D --> G["材质组<br/>/api/v1/texture/*"]
D --> H["档案组<br/>/api/v1/profile/*"]
D --> I["验证码组<br/>/api/v1/captcha/*"]
D --> J["Yggdrasil组<br/>/api/v1/yggdrasil/*"]
D --> K["系统组<br/>/api/v1/system/*"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L10-L118)
- [swagger.go](file://internal/handler/swagger.go#L41-L63)
章节来源
- [routes.go](file://internal/handler/routes.go#L10-L118)
- [swagger.go](file://internal/handler/swagger.go#L41-L63)
## 核心组件
- 路由注册器:按版本分组,按需挂载鉴权中间件
- Swagger提供交互式文档与健康检查
- 鉴权中间件统一从Authorization头解析Bearer Token并注入用户上下文
- 响应模型:统一的响应结构与分页结构
- 类型定义:请求/响应结构体与约束规则
章节来源
- [routes.go](file://internal/handler/routes.go#L10-L118)
- [swagger.go](file://internal/handler/swagger.go#L41-L63)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [response.go](file://internal/model/response.go#L1-L86)
- [common.go](file://internal/types/common.go#L1-L215)
## 架构总览
下图展示API调用链与鉴权流程
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Router as "Gin路由"
participant MW as "鉴权中间件"
participant Handler as "处理器"
participant Service as "服务层"
participant DB as "数据库/缓存"
Client->>Router : "HTTP请求"
Router->>MW : "匹配路由并执行中间件"
MW->>MW : "解析Authorization头"
MW->>MW : "校验JWT并注入用户上下文"
MW-->>Router : "通过或拒绝"
Router->>Handler : "转发到对应处理器"
Handler->>Service : "调用业务逻辑"
Service->>DB : "读写数据"
DB-->>Service : "返回结果"
Service-->>Handler : "返回业务结果"
Handler-->>Client : "统一响应结构"
```
图表来源
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [routes.go](file://internal/handler/routes.go#L10-L118)
## 详细组件分析
### 认证组(/api/v1/auth
- 无需JWT
- 端点
- POST /api/v1/auth/register
- 请求体RegisterRequest
- 响应LoginResponse含token与用户信息
- 错误400参数错误、401登录失败
- POST /api/v1/auth/login
- 请求体LoginRequest支持用户名或邮箱
- 响应LoginResponse
- 错误400参数错误、401登录失败
- POST /api/v1/auth/send-code
- 请求体SendVerificationCodeRequest类型枚举register/reset_password/change_email
- 响应:通用成功响应
- 错误400参数错误
- POST /api/v1/auth/reset-password
- 请求体ResetPasswordRequest验证码6位
- 响应:通用成功响应
- 错误400参数错误
参数与校验要点
- RegisterRequest用户名、邮箱、密码、验证码、可选头像URL
- LoginRequest用户名或邮箱、密码
- SendVerificationCodeRequest邮箱、类型
- ResetPasswordRequest邮箱、验证码、新密码
章节来源
- [routes.go](file://internal/handler/routes.go#L18-L25)
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
- [common.go](file://internal/types/common.go#L27-L66)
### 用户组(/api/v1/user需JWT
- GET /api/v1/user/profile
- 响应UserInfo
- 错误401未授权、404用户不存在
- PUT /api/v1/user/profile
- 请求体UpdateUserRequest修改密码需同时提供旧密码与新密码
- 响应UserInfo
- 错误400参数错误、401未授权、404用户不存在、500服务器错误
- POST /api/v1/user/avatar/upload-url
- 请求体GenerateAvatarUploadURLRequest文件名
- 响应GenerateAvatarUploadURLResponse含post_url、form_data、avatar_url、expires_in
- 错误400参数错误
- PUT /api/v1/user/avatar
- 查询参数avatar_url
- 响应UserInfo
- 错误400参数错误
- POST /api/v1/user/change-email
- 请求体ChangeEmailRequest新邮箱+验证码)
- 响应UserInfo
- 错误400参数错误、401未授权
参数与校验要点
- UpdateUserRequest头像URL、旧密码、新密码二者需同时提供
- GenerateAvatarUploadURLRequest文件名必填
- ChangeEmailRequest新邮箱、验证码
章节来源
- [routes.go](file://internal/handler/routes.go#L27-L41)
- [user_handler.go](file://internal/handler/user_handler.go#L17-L68)
- [user_handler.go](file://internal/handler/user_handler.go#L70-L193)
- [user_handler.go](file://internal/handler/user_handler.go#L195-L253)
- [user_handler.go](file://internal/handler/user_handler.go#L255-L326)
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [common.go](file://internal/types/common.go#L42-L80)
- [common.go](file://internal/types/common.go#L107-L125)
- [common.go](file://internal/types/common.go#L167-L179)
### 材质组(/api/v1/texture
- 公开端点无需JWT
- GET /api/v1/texture
- 查询keyword、typeSKIN/CAPE、public_only、page、page_size
- 响应分页响应列表项为TextureInfo
- GET /api/v1/texture/{id}
- 路径参数id
- 响应TextureInfo
- 需JWT端点
- POST /api/v1/texture/upload-url
- 请求体GenerateTextureUploadURLRequest文件名、纹理类型
- 响应GenerateTextureUploadURLResponse含post_url、form_data、texture_url、expires_in
- 错误400参数错误
- POST /api/v1/texture
- 请求体CreateTextureRequest名称、描述、类型、URL、哈希、大小、公开性、是否Slim
- 响应TextureInfo
- 错误400参数错误
- PUT /api/v1/texture/{id}
- 路径参数id请求体UpdateTextureRequest名称、描述、公开性
- 响应TextureInfo
- 错误400参数错误、403无权操作
- DELETE /api/v1/texture/{id}
- 路径参数id
- 响应:通用成功响应
- 错误400参数错误、403无权操作
- POST /api/v1/texture/{id}/favorite
- 路径参数id
- 响应:布尔值(是否收藏)
- 错误400参数错误
- GET /api/v1/texture/my
- 查询page、page_size
- 响应分页响应列表项为TextureInfo
- GET /api/v1/texture/favorites
- 查询page、page_size
- 响应分页响应列表项为TextureInfo
参数与校验要点
- GenerateTextureUploadURLRequest文件名必填、类型枚举SKIN/CAPE
- CreateTextureRequest名称必填、URL必填且为有效URL、哈希长度64、大小>0、类型枚举
- UpdateTextureRequest名称长度限制、公开性可选
- 搜索与分页默认page=1、page_size默认20、最大100
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
- [texture_handler.go](file://internal/handler/texture_handler.go#L174-L223)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
- [texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L599)
- [common.go](file://internal/types/common.go#L81-L105)
- [common.go](file://internal/types/common.go#L181-L191)
- [common.go](file://internal/types/common.go#L193-L200)
### 档案组(/api/v1/profile
- 公开端点无需JWT
- GET /api/v1/profile/{uuid}
- 路径参数uuid
- 响应ProfileInfo
- 错误404档案不存在、500服务器错误
- 需JWT端点
- POST /api/v1/profile
- 请求体CreateProfileRequest名称
- 响应ProfileInfo含自动生成UUID
- 错误400参数错误或已达上限、401未授权、500服务器错误
- GET /api/v1/profile
- 响应ProfileInfo列表
- 错误401未授权、500服务器错误
- PUT /api/v1/profile/{uuid}
- 路径参数uuid请求体UpdateProfileRequest名称、皮肤ID、斗篷ID
- 响应ProfileInfo
- 错误400参数错误、401未授权、403无权操作、404档案不存在、500服务器错误
- DELETE /api/v1/profile/{uuid}
- 路径参数uuid
- 响应:通用成功响应
- 错误401未授权、403无权操作、404档案不存在、500服务器错误
- POST /api/v1/profile/{uuid}/activate
- 路径参数uuid
- 响应:通用成功响应
- 错误401未授权、403无权操作、404档案不存在、500服务器错误
参数与校验要点
- CreateProfileRequest名称长度1-16
- UpdateProfileRequest名称长度1-16皮肤/斗篷ID可选
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L93)
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L280)
- [profile_handler.go](file://internal/handler/profile_handler.go#L282-L339)
- [profile_handler.go](file://internal/handler/profile_handler.go#L341-L399)
- [common.go](file://internal/types/common.go#L81-L85)
- [common.go](file://internal/types/common.go#L201-L207)
### 验证码组(/api/v1/captcha
- GET /api/v1/captcha/generate
- 响应主图、滑块图、验证码ID、Y坐标
- 错误500生成失败
- POST /api/v1/captcha/verify
- 请求体:{captchaId, dx}
- 响应:验证结果(成功/失败)
- 错误400参数错误、500验证失败
章节来源
- [routes.go](file://internal/handler/routes.go#L80-L85)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L11-L34)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L36-L77)
### Yggdrasil组/api/v1/yggdrasil
- 元数据
- GET /api/v1/yggdrasil
- 响应:实现信息、链接、特性开关、签名公钥、皮肤域名
- 认证服务
- POST /api/v1/yggdrasil/authserver/authenticate
- 请求体AuthenticateRequestagent、clientToken、identifier、password、requestUser
- 响应AccessToken、ClientToken、SelectedProfile、AvailableProfiles、User可选
- POST /api/v1/yggdrasil/authserver/validate
- 请求体ValidTokenRequestaccessToken、clientToken
- 响应204有效或403无效
- POST /api/v1/yggdrasil/authserver/refresh
- 请求体RefreshRequestaccessToken、clientToken、requestUser、selectedProfile
- 响应新的AccessToken、ClientToken、SelectedProfile、User可选
- POST /api/v1/yggdrasil/authserver/invalidate
- 请求体ValidTokenRequestaccessToken、clientToken
- 响应204
- POST /api/v1/yggdrasil/authserver/signout
- 请求体SignOutRequestusername、password
- 响应204
- 会话服务
- GET /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid}
- 响应:序列化后的档案信息
- POST /api/v1/yggdrasil/sessionserver/session/minecraft/join
- 请求体JoinServerRequestserverId、accessToken、selectedProfile
- 响应204 或错误
- GET /api/v1/yggdrasil/sessionserver/session/minecraft/hasJoined
- 查询serverId、username、ip
- 响应200Profile或204无内容
- API服务
- POST /api/v1/yggdrasil/api/profiles/minecraft
- 请求体:字符串数组(玩家名)
- 响应Profile列表
参数与校验要点
- 认证/验证/刷新/登出:均需严格校验请求体字段与类型
- 会话验证serverId与username必填
- 批量查询:名称数组非空
章节来源
- [routes.go](file://internal/handler/routes.go#L87-L111)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L246)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L248-L267)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L269-L361)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L363-L378)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L380-L425)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L427-L447)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L554-L587)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L589-L616)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L618-L666)
### 系统组(/api/v1/system
- GET /api/v1/system/config
- 响应:站点名称、描述、注册开关、每用户最大材质数、每用户最大档案数
- 注意:当前实现为占位返回固定配置
章节来源
- [routes.go](file://internal/handler/routes.go#L112-L118)
- [routes.go](file://internal/handler/routes.go#L120-L140)
## 依赖分析
- 路由与中间件
- 路由文件按组挂载鉴权中间件确保受保护端点仅接受有效JWT
- 处理器与服务
- 处理器通过服务层完成业务逻辑,服务层再与数据库/缓存交互
- 响应与类型
- 统一响应结构与分页结构,类型定义集中约束参数范围与格式
```mermaid
graph LR
Routes["路由注册<br/>routes.go"] --> MW["鉴权中间件<br/>auth.go"]
MW --> Handlers["各处理器<br/>auth/user/texture/profile/captcha/yggdrasil"]
Handlers --> Services["服务层"]
Services --> DB["数据库/缓存"]
Handlers --> Models["响应模型<br/>response.go"]
Handlers --> Types["类型定义<br/>common.go"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L10-L118)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [response.go](file://internal/model/response.go#L1-L86)
- [common.go](file://internal/types/common.go#L1-L215)
章节来源
- [routes.go](file://internal/handler/routes.go#L10-L118)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [response.go](file://internal/model/response.go#L1-L86)
- [common.go](file://internal/types/common.go#L1-L215)
## 性能考虑
- 上传URL预签名
- 材质与头像上传URL均采用预签名策略缩短上传路径、降低后端压力
- 分页与限制
- 材质与档案列表默认分页,避免一次性返回大量数据
- 材质与档案数量限制(当前代码中硬编码,建议从配置读取)
- 缓存与日志
- Redis用于验证码与公钥等缓存减少数据库压力
- 日志记录关键错误,便于追踪与优化
- 并发与超时
- 建议为外部服务(如对象存储)设置合理超时与重试策略
## 故障排查指南
- 鉴权失败
- 缺少Authorization头或格式不正确401
- 无效token401
- 参数错误
- JSON绑定失败或字段校验不通过400
- 资源不存在
- 用户、档案、材质不存在404
- 权限不足
- 非本人操作403
- 服务器错误
- 数据库异常、外部服务不可用500
章节来源
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [response.go](file://internal/model/response.go#L27-L52)
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
- [user_handler.go](file://internal/handler/user_handler.go#L70-L193)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L280)
## 结论
本API参考基于现有路由与处理器注释梳理了CarrotSkin后端的核心REST接口与鉴权机制。建议在生产环境中
- 将硬编码限制与默认值迁移至配置中心
- 引入速率限制与防刷策略
- 对敏感接口增加审计日志
- 完善单元测试与集成测试覆盖
## 附录
### 统一响应结构
- 成功响应包含code、message、data
- 分页响应包含code、message、data、total、page、per_page
- 错误响应包含code、message、error开发环境
章节来源
- [response.go](file://internal/model/response.go#L1-L86)
### 常见错误码
- 200成功
- 400请求参数错误
- 401未授权
- 403禁止访问
- 404资源不存在
- 500服务器内部错误
章节来源
- [response.go](file://internal/model/response.go#L27-L52)
### Swagger与健康检查
- 文档地址:/swagger/*any
- 健康检查:/health
章节来源
- [swagger.go](file://internal/handler/swagger.go#L41-L63)

View File

@@ -1,386 +0,0 @@
# Yggdrasil协议API
<cite>
**本文档引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go)
- [yggdrasil.go](file://internal/model/yggdrasil.go)
- [token.go](file://internal/model/token.go)
- [user_service.go](file://internal/service/user_service.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [jwt.go](file://pkg/auth/jwt.go)
</cite>
## 目录
1. [简介](#简介)
2. [Yggdrasil路由结构](#yggdrasil路由结构)
3. [认证服务API](#认证服务api)
4. [会话服务API](#会话服务api)
5. [档案服务API](#档案服务api)
6. [与Minecraft客户端交互流程](#与minecraft客户端交互流程)
7. [内部用户系统集成](#内部用户系统集成)
8. [错误处理](#错误处理)
9. [元数据与证书](#元数据与证书)
## 简介
Yggdrasil协议是Minecraft客户端认证的核心接口本系统实现了完整的Yggdrasil协议集成为Minecraft玩家提供认证、会话管理和档案查询服务。该实现通过`/yggdrasil`路由组暴露标准API端点与Minecraft客户端无缝对接同时将Yggdrasil请求映射到系统的内部用户体系。
**Section sources**
- [routes.go](file://internal/handler/routes.go#L87-L111)
## Yggdrasil路由结构
系统在`/api/v1/yggdrasil`路径下提供了完整的Yggdrasil协议支持包含三个主要子路由组`authserver``sessionserver``profiles`,分别处理认证、会话和档案相关请求。
```mermaid
graph TB
Yggdrasil[Yggdrasil根路由 /yggdrasil]
subgraph AuthServer
Authenticate[POST /authserver/authenticate]
Validate[POST /authserver/validate]
Refresh[POST /authserver/refresh]
Invalidate[POST /authserver/invalidate]
SignOut[POST /authserver/signout]
end
subgraph SessionServer
GetProfile[GET /sessionserver/session/minecraft/profile/:uuid]
JoinServer[POST /sessionserver/session/minecraft/join]
HasJoined[GET /sessionserver/session/minecraft/hasJoined]
end
subgraph Profiles
GetProfilesByName[POST /api/profiles/minecraft]
end
Yggdrasil --> Authenticate
Yggdrasil --> Validate
Yggdrasil --> Refresh
Yggdrasil --> Invalidate
Yggdrasil --> SignOut
Yggdrasil --> GetProfile
Yggdrasil --> JoinServer
Yggdrasil --> HasJoined
Yggdrasil --> GetProfilesByName
```
**Diagram sources **
- [routes.go](file://internal/handler/routes.go#L87-L111)
**Section sources**
- [routes.go](file://internal/handler/routes.go#L87-L111)
## 认证服务API
认证服务API位于`/yggdrasil/authserver`路径下,提供用户认证和令牌管理功能。
### authenticate
用户认证端点,用于验证用户凭据并获取访问令牌。
- **方法**: POST
- **路径**: `/yggdrasil/authserver/authenticate`
- **请求体**:
```json
{
"username": "用户邮箱或用户名",
"password": "密码",
"clientToken": "客户端令牌"
}
```
- **响应**:
- 200: 返回包含`accessToken`、`availableProfiles`和`selectedProfile`的认证响应
- 403: 认证失败
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L246)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L204-L209)
### validate
验证访问令牌的有效性。
- **方法**: POST
- **路径**: `/yggdrasil/authserver/validate`
- **请求体**:
```json
{
"accessToken": "访问令牌"
}
```
- **响应**:
- 204: 令牌有效
- 403: 令牌无效
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L248-L267)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L260-L261)
### refresh
刷新访问令牌,延长会话有效期。
- **方法**: POST
- **路径**: `/yggdrasil/authserver/refresh`
- **请求体**:
```json
{
"accessToken": "当前访问令牌",
"clientToken": "客户端令牌",
"selectedProfile": "选定的档案"
}
```
- **响应**:
- 200: 返回新的`accessToken`和`selectedProfile`
- 400: 刷新失败
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L269-L361)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L341-L351)
### invalidate
使访问令牌失效。
- **方法**: POST
- **路径**: `/yggdrasil/authserver/invalidate`
- **请求体**:
```json
{
"accessToken": "要使失效的令牌"
}
```
- **响应**:
- 204: 成功使令牌失效
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L363-L378)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L375)
### signout
用户登出,使该用户的所有令牌失效。
- **方法**: POST
- **路径**: `/yggdrasil/authserver/signout`
- **请求体**:
```json
{
"username": "用户邮箱",
"password": "密码"
}
```
- **响应**:
- 204: 登出成功
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L380-L425)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L422)
## 会话服务API
会话服务API位于`/yggdrasil/sessionserver`路径下,管理玩家与服务器的会话。
### JoinServer
记录玩家加入服务器的会话信息。
- **方法**: POST
- **路径**: `/yggdrasil/sessionserver/session/minecraft/join`
- **请求体**:
```json
{
"accessToken": "访问令牌",
"selectedProfile": "选定的档案UUID",
"serverId": "服务器ID"
}
```
- **响应**:
- 204: 加入成功
- 500: 加入失败
```mermaid
sequenceDiagram
participant Client as "Minecraft客户端"
participant SessionServer as "会话服务"
participant Redis as "Redis存储"
Client->>SessionServer : POST /join
SessionServer->>SessionServer : 验证accessToken
SessionServer->>SessionServer : 验证selectedProfile
SessionServer->>Redis : 存储会话数据
Redis-->>SessionServer : 存储成功
SessionServer-->>Client : 204 No Content
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
### HasJoinedServer
验证玩家是否已加入指定服务器。
- **方法**: GET
- **路径**: `/yggdrasil/sessionserver/session/minecraft/hasJoined`
- **查询参数**:
- `serverId`: 服务器ID
- `username`: 用户名
- `ip`: IP地址可选
- **响应**:
- 200: 返回玩家档案信息
- 204: 未加入或验证失败
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
## 档案服务API
档案服务API提供玩家档案查询功能。
### GetProfileByUUID
根据UUID获取玩家档案。
- **方法**: GET
- **路径**: `/yggdrasil/sessionserver/session/minecraft/profile/:uuid`
- **响应**:
- 200: 返回玩家档案信息
- 500: 获取失败
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L427-L447)
- [profile_service.go](file://internal/service/profile_service.go#L71-L80)
### GetProfilesByName
根据用户名批量获取玩家档案。
- **方法**: POST
- **路径**: `/yggdrasil/api/profiles/minecraft`
- **请求体**: 用户名数组
```json
["player1", "player2"]
```
- **响应**: 返回匹配的档案列表
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L554-L587)
- [profile_repository.go](file://internal/repository/profile_repository.go#L129-L137)
## 与Minecraft客户端交互流程
Minecraft客户端与Yggdrasil服务的完整交互流程如下
```mermaid
sequenceDiagram
participant Client as "Minecraft客户端"
participant AuthServer as "认证服务"
participant SessionServer as "会话服务"
participant ProfileServer as "档案服务"
Client->>AuthServer : authenticate(邮箱/用户名, 密码)
AuthServer-->>Client : accessToken, availableProfiles
Client->>SessionServer : join(serverId, accessToken, selectedProfile)
SessionServer-->>Client : 204
Client->>ProfileServer : hasJoined(serverId, username)
ProfileServer-->>Client : 玩家档案
Client->>ProfileServer : getProfile(uuid)
ProfileServer-->>Client : 档案详情
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
## 内部用户系统集成
Yggdrasil实现与系统内部用户体系的集成通过以下方式完成
### 用户映射
系统通过`Yggdrasil`模型将Yggdrasil协议的密码认证与内部用户ID关联。当用户注册时系统自动生成一个16位随机密码与用户ID绑定。
```mermaid
classDiagram
class User {
+int64 ID
+string Username
+string Email
+string Password
}
class Yggdrasil {
+int64 ID
+string Password
}
User --> Yggdrasil : "1对1关联"
```
**Diagram sources **
- [yggdrasil.go](file://internal/model/yggdrasil.go)
- [user_service.go](file://internal/service/user_service.go#L13-L67)
**Section sources**
- [yggdrasil.go](file://internal/model/yggdrasil.go#L13-L38)
- [user_service.go](file://internal/service/user_service.go#L13-L67)
### 令牌管理
系统使用`Token`模型管理访问令牌,将`accessToken`与`userID`、`profileId`关联,实现会话状态管理。
```mermaid
classDiagram
class Token {
+string AccessToken
+int64 UserID
+string ProfileId
+bool Usable
}
class Profile {
+string UUID
+int64 UserID
+string Name
}
Token --> Profile : "关联档案"
Token --> User : "关联用户"
```
**Diagram sources **
- [token.go](file://internal/model/token.go)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L211-L216)
**Section sources**
- [token.go](file://internal/model/token.go)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L211-L216)
## 错误处理
系统实现了标准化的错误处理机制所有Yggdrasil端点返回一致的错误响应格式。
### 常见错误代码
| HTTP状态码 | 错误类型 | 描述 |
|-----------|---------|------|
| 400 | Bad Request | 请求格式无效 |
| 403 | Forbidden | 认证失败或权限不足 |
| 404 | Not Found | 资源未找到 |
| 500 | Internal Server Error | 服务器内部错误 |
### 错误响应示例
```json
{
"error": "密码错误"
}
```
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L19-L57)
## 元数据与证书
系统提供元数据和玩家证书服务支持Minecraft客户端的高级功能。
### GetMetaData
获取服务元数据。
- **方法**: GET
- **路径**: `/yggdrasil`
- **响应**: 包含实现信息、链接和功能标志
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L589-L615)
### GetPlayerCertificates
获取玩家证书。
- **方法**: POST
- **路径**: `/yggdrasil/minecraftservices/player/certificates`
- **认证**: Bearer Token
- **响应**: 包含玩家公钥证书
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L618-L667)
- [profile_service.go](file://internal/service/profile_service.go#L44-L48)

View File

@@ -1,305 +0,0 @@
# 会话服务
<cite>
**本文档引用文件**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go)
- [routes.go](file://internal/handler/routes.go)
- [profile.go](file://internal/model/profile.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [redis.go](file://pkg/redis/redis.go)
</cite>
## 目录
1. [简介](#简介)
2. [API路由结构](#api路由结构)
3. [核心API功能详解](#核心api功能详解)
4. [会话数据结构](#会话数据结构)
5. [反作弊机制](#反作弊机制)
6. [与Minecraft客户端交互流程](#与minecraft客户端交互流程)
7. [错误处理与日志](#错误处理与日志)
8. [测试验证](#测试验证)
## 简介
本文档详细描述了Yggdrasil会话服务的核心功能重点聚焦于`/sessionserver`路由组下的三个关键API`GetProfileByUUID``JoinServer``HasJoinedServer`。这些API构成了Minecraft服务器会话验证系统的核心负责处理玩家加入服务器的会话建立、验证和玩家档案查询。
系统通过Redis存储会话数据利用`Join_`为前缀的键名和15分钟的TTL生存时间来管理会话生命周期。整个流程确保了只有经过身份验证的玩家才能加入服务器同时通过IP地址和用户名的双重验证机制防止作弊行为。
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L1-L100)
- [routes.go](file://internal/handler/routes.go#L87-L111)
## API路由结构
会话服务的API路由定义在`routes.go`文件中,位于`/yggdrasil/sessionserver`路径下。该路由组提供了三个核心端点:
- `GET /session/minecraft/profile/:uuid`根据玩家UUID获取其公开档案信息。
- `POST /session/minecraft/join`:接收客户端的会话信息,建立服务器会话。
- `GET /session/minecraft/hasJoined`:验证某玩家是否已成功加入指定服务器。
```mermaid
graph TB
subgraph "Yggdrasil API"
A[/yggdrasil/sessionserver]
A --> B[GET /session/minecraft/profile/:uuid]
A --> C[POST /session/minecraft/join]
A --> D[GET /session/minecraft/hasJoined]
end
```
**Diagram sources **
- [routes.go](file://internal/handler/routes.go#L100-L105)
**Section sources**
- [routes.go](file://internal/handler/routes.go#L87-L105)
## 核心API功能详解
### JoinServer API
`JoinServer` API是建立服务器会话的核心。当Minecraft客户端成功登录后会调用此API来“加入”一个特定的服务器。
**功能流程:**
1. **接收参数**API接收`serverId``accessToken``selectedProfile`玩家UUID三个必需参数。
2. **输入验证**:对`serverId`进行格式检查长度不超过100字符不包含`<>\"'&`等危险字符并验证客户端IP地址格式。
3. **令牌验证**:通过`accessToken`在数据库中查找对应的令牌记录,确保令牌有效。
4. **配置文件匹配**:将`selectedProfile`客户端提供的UUID与令牌中关联的`ProfileId`进行比对,确保玩家使用的是正确的配置文件。
5. **构建会话数据**从数据库中获取该UUID对应的玩家档案提取其用户名`Name`)。
6. **存储会话**:将`accessToken``userName``selectedProfile`和客户端IP地址构建成`SessionData`结构体序列化后存入Redis。存储的键名为`Join_` + `serverId`TTL设置为15分钟。
```mermaid
sequenceDiagram
participant Client as "Minecraft客户端"
participant Handler as "yggdrasil_handler"
participant Service as "yggdrasil_service"
participant Redis as "Redis"
participant DB as "数据库"
Client->>Handler : POST /join (serverId, accessToken, selectedProfile)
Handler->>Service : JoinServer(serverId, accessToken, selectedProfile, clientIP)
Service->>DB : 根据accessToken查找Token
DB-->>Service : Token信息
Service->>Service : 验证selectedProfile与Token匹配
Service->>DB : 根据UUID查找Profile
DB-->>Service : Profile (含用户名)
Service->>Service : 构建SessionData对象
Service->>Service : 序列化SessionData
Service->>Redis : Set(Join_serverId, marshaledData, 15min)
Redis-->>Service : 成功
Service-->>Handler : 成功
Handler-->>Client : HTTP 204 No Content
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
### HasJoinedServer API
`HasJoinedServer` API用于服务器端验证一个玩家是否已经通过了会话验证。
**功能流程:**
1. **接收参数**API接收`serverId``username`两个必需参数,以及可选的`ip`参数。
2. **输入验证**:确保`serverId``username`不为空。
3. **获取会话数据**:使用`Join_` + `serverId`作为键名从Redis中获取之前存储的会话数据。
4. **反序列化**将获取到的JSON数据反序列化为`SessionData`结构体。
5. **验证匹配**
- **用户名匹配**:比较会话数据中的`UserName`与请求中的`username`是否完全一致(区分大小写)。
- **IP地址匹配**:如果请求中提供了`ip`参数,则会比较会话数据中的`IP`与请求的`ip`是否一致。
6. **返回结果**:如果所有验证通过,则从数据库中获取该`username`对应的完整玩家档案(包括皮肤、披风等信息)并返回;否则返回验证失败。
```mermaid
sequenceDiagram
participant Server as "Minecraft服务器"
participant Handler as "yggdrasil_handler"
participant Service as "yggdrasil_service"
participant Redis as "Redis"
participant DB as "数据库"
Server->>Handler : GET /hasJoined?serverId=...&username=...&ip=...
Handler->>Service : HasJoinedServer(serverId, username, ip)
Service->>Redis : Get(Join_serverId)
Redis-->>Service : marshaledData (或 nil)
alt 会话不存在
Service-->>Handler : 错误
Handler-->>Server : HTTP 204 No Content
else 会话存在
Service->>Service : 反序列化为SessionData
Service->>Service : 验证UserName == username
Service->>Service : 验证IP (如果提供)
alt 验证失败
Service-->>Handler : 错误
Handler-->>Server : HTTP 204 No Content
else 验证成功
Service->>DB : 根据username查找Profile
DB-->>Service : Profile信息
Service-->>Handler : Profile
Handler-->>Server : HTTP 200 + Profile JSON
end
end
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
### GetProfileByUUID API
`GetProfileByUUID` API提供了一种通过玩家UUID查询其公开档案信息的方式。
**功能流程:**
1. **接收参数**从URL路径中获取`:uuid`参数。
2. **格式化UUID**:调用`utils.FormatUUID`函数将可能存在的十六进制格式UUID转换为标准的带连字符格式。
3. **查询档案**:调用`service.GetProfileByUUID`方法根据格式化后的UUID在数据库中查找对应的`Profile`记录。
4. **序列化响应**:将`Profile`模型转换为包含皮肤Skin和披风CapeURL的`ProfileResponse`结构体。
5. **返回结果**将序列化后的JSON数据返回给客户端。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "yggdrasil_handler"
participant Service as "profile_service"
participant DB as "数据库"
Client->>Handler : GET /profile/ : uuid
Handler->>Handler : FormatUUID(uuid)
Handler->>Service : GetProfileByUUID(uuid)
Service->>DB : FindProfileByUUID(uuid)
DB-->>Service : Profile
Service-->>Handler : Profile
Handler->>Handler : SerializeProfile(Profile)
Handler-->>Client : HTTP 200 + Profile JSON
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L427-L447)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L427-L447)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
## 会话数据结构
`SessionData`结构体定义了存储在Redis中的会话信息`JoinServer``HasJoinedServer`两个API之间通信的核心载体。
```go
type SessionData struct {
AccessToken string `json:"accessToken"`
UserName string `json:"userName"`
SelectedProfile string `json:"selectedProfile"`
IP string `json:"ip"`
}
```
- **AccessToken**:客户端的访问令牌,用于在`JoinServer`时验证身份。
- **UserName**:玩家的用户名(如`Steve`),在`HasJoinedServer`时用于比对。
- **SelectedProfile**玩家的UUID用于唯一标识玩家。
- **IP**客户端的IP地址用于反作弊验证。
该结构体在`JoinServer`时被创建并序列化存储,在`HasJoinedServer`时被反序列化读取并用于验证。
```mermaid
classDiagram
class SessionData {
+string accessToken
+string userName
+string selectedProfile
+string ip
}
```
**Diagram sources **
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L25-L30)
**Section sources**
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L25-L30)
## 反作弊机制
系统通过`HasJoinedServer` API实现了有效的反作弊机制主要依赖于以下两个层面的验证
1. **令牌绑定验证**:在`JoinServer`阶段,系统强制要求`selectedProfile`UUID必须与`accessToken`所关联的配置文件ID完全匹配。这确保了玩家不能使用他人的令牌来冒充身份。
2. **IP地址与时间戳验证**
- **IP地址验证**`HasJoinedServer` API可以选择性地接收一个`ip`参数。如果提供了该参数,系统会将其与`JoinServer`时记录的IP地址进行比对。如果两者不一致则验证失败。这可以有效防止玩家在一台机器上登录后将令牌分享给另一台机器上的其他玩家使用。
- **时间戳验证**通过将Redis中会话数据的TTL设置为15分钟系统实现了会话的自动过期。这意味着即使令牌和IP验证通过该会话也只在有限时间内有效增加了作弊的难度和成本。
**Section sources**
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L195-L198)
## 与Minecraft客户端交互流程
以下是Minecraft客户端与本会话服务交互的完整流程示例
1. **客户端登录**玩家在Minecraft启动器中输入邮箱和密码启动器调用`/authserver/authenticate` API进行身份验证并获取`accessToken``availableProfiles`
2. **选择配置文件**:启动器列出可用的配置文件,玩家选择一个(如`Steve`)。
3. **加入服务器**
- 玩家在游戏内选择一个服务器并点击“加入”。
- 启动器调用`/sessionserver/session/minecraft/join` API携带`serverId`(服务器的哈希值)、`accessToken``selectedProfile``Steve`的UUID
- 服务端验证信息无误后,将包含`accessToken``userName``Steve`)、`selectedProfile`和客户端IP的`SessionData`存入Redis键名为`Join_` + `serverId`
4. **服务器验证**
- 游戏客户端连接到Minecraft服务器。
- 服务器向本会话服务的`/sessionserver/session/minecraft/hasJoined` API发起请求携带`serverId``username``Steve`和客户端IP。
- 服务端查找Redis中对应的会话数据并验证`userName``ip`是否匹配。
- 如果验证通过,服务端返回`Steve`的完整档案信息包括皮肤URL服务器允许玩家加入游戏否则连接被拒绝。
```mermaid
flowchart TD
A[Minecraft客户端] --> |1. authenticate| B[/authserver/authenticate]
B --> C{获取 accessToken 和 UUID}
C --> D[选择配置文件]
D --> E[点击加入服务器]
E --> |3. join| F[/sessionserver/join]
F --> G[Redis: 存储会话]
E --> H[Minecraft服务器]
H --> |4. hasJoined| I[/sessionserver/hasJoined]
I --> J[Redis: 查找会话]
J --> K{验证通过?}
K --> |是| L[返回玩家档案]
L --> M[允许加入游戏]
K --> |否| N[拒绝连接]
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L552)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L201)
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L552)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L201)
## 错误处理与日志
系统在关键操作点都集成了详细的错误处理和日志记录:
- **输入验证**对所有API的输入参数进行严格校验如空值、格式错误等并返回清晰的错误信息。
- **业务逻辑错误**:对于令牌无效、配置文件不匹配、用户名不匹配等情况,返回特定的错误码和消息。
- **系统错误**对数据库查询失败、Redis操作失败、JSON序列化/反序列化失败等底层错误进行捕获和记录。
- **日志记录**:使用`zap`日志库,对关键操作(如“玩家成功加入服务器”、“会话验证失败”)进行结构化日志记录,便于问题追踪和审计。
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L459-L484)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L103-L155)
## 测试验证
系统的功能通过单元测试得到了充分验证,确保了核心逻辑的正确性。
- **常量验证**:测试确认`SessionKeyPrefix`常量值为`"Join_"``SessionTTL`为15分钟。
- **输入验证**:对`JoinServer``HasJoinedServer`的输入参数(空值、格式)进行了全面的边界测试。
- **逻辑验证**:测试了`JoinServer`的会话键生成逻辑,确保`serverId`能正确拼接成`Join_serverId`的格式。
- **匹配逻辑**:验证了`HasJoinedServer`的用户名和IP地址匹配逻辑确保大小写敏感和IP比对的正确性。
```mermaid
graph TD
A[测试用例] --> B[TestYggdrasilService_Constants]
A --> C[TestJoinServer_InputValidation]
A --> D[TestHasJoinedServer_InputValidation]
A --> E[TestJoinServer_SessionKey]
A --> F[TestHasJoinedServer_UsernameMatching]
A --> G[TestHasJoinedServer_IPMatching]
```
**Diagram sources **
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go#L10-L350)
**Section sources**
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go#L10-L350)

View File

@@ -1,309 +0,0 @@
# 档案查询服务
<cite>
**本文档引用文件**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [profile.go](file://internal/model/profile.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [yggdrasil_handler_test.go](file://internal/handler/yggdrasil_handler_test.go)
- [texture.go](file://internal/model/texture.go)
- [routes.go](file://internal/handler/routes.go)
</cite>
## 目录
1. [简介](#简介)
2. [核心功能](#核心功能)
3. [/api/profiles/minecraft 端点详解](#apiprofilesminecraft-端点详解)
4. [/profile/:uuid 与 /api/profiles/minecraft 的区别](#profileuuid-与-apiprofilesminecraft-的区别)
5. [皮肤与披风类型标识](#皮肤与披风类型标识)
6. [请求与响应示例](#请求与响应示例)
7. [应用场景](#应用场景)
## 简介
本服务为Minecraft Yggdrasil认证系统的一部分提供档案Profile查询功能。核心功能包括根据用户名批量查询UUID和档案信息以及根据UUID获取单个档案的完整详情。该服务支持Minecraft客户端在登录和聊天时加载玩家数据。
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L1-L667)
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
## 核心功能
档案查询服务主要提供两种查询方式:
1. 批量查询通过用户名列表获取对应的UUID和基础档案信息
2. 详情查询通过UUID获取单个档案的完整信息包括皮肤和披风等纹理数据
这些功能通过Yggdrasil API的特定端点实现支持Minecraft客户端的数据加载需求。
```mermaid
graph TD
A[客户端请求] --> B{请求类型}
B --> |批量查询| C[/api/profiles/minecraft]
B --> |详情查询| D[/profile/:uuid]
C --> E[返回UUID和基础信息]
D --> F[返回完整档案信息]
```
**Diagram sources **
- [routes.go](file://internal/handler/routes.go#L108-L109)
- [profile_handler.go](file://internal/handler/profile_handler.go#L164-L195)
## /api/profiles/minecraft 端点详解
`/api/profiles/minecraft` 端点用于根据玩家用户名列表批量查询对应的UUID和公开档案信息。
### 功能说明
该端点允许客户端一次性查询多个玩家的档案信息,主要用于:
- 游戏登录时预加载玩家数据
- 聊天系统中显示玩家名称和头像
- 服务器列表中显示在线玩家信息
### 实现流程
```mermaid
sequenceDiagram
participant Client as 客户端
participant Handler as yggdrasil_handler
participant Service as profile_service
participant Repository as profile_repository
participant DB as 数据库
Client->>Handler : POST /api/profiles/minecraft
Handler->>Handler : 解析用户名数组
Handler->>Service : GetProfilesDataByNames(names)
Service->>Repository : GetProfilesByNames(names)
Repository->>DB : SELECT * FROM profiles WHERE name IN (?)
DB-->>Repository : 返回档案列表
Repository-->>Service : 返回档案列表
Service-->>Handler : 返回档案列表
Handler-->>Client : 返回JSON响应
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
- [profile_service.go](file://internal/service/profile_service.go#L237-L243)
- [profile_repository.go](file://internal/repository/profile_repository.go#L129-L137)
### 请求参数
- **方法**: POST
- **路径**: `/api/profiles/minecraft`
- **内容类型**: `application/json`
- **请求体**: 包含用户名字符串数组
### 错误处理
该端点会处理以下错误情况:
- 请求体格式无效
- 用户名数组为空
- 数据库查询失败
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
- [profile_service.go](file://internal/service/profile_service.go#L237-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L129-L137)
## /profile/:uuid 与 /api/profiles/minecraft 的区别
这两个端点虽然都用于查询玩家档案,但功能和用途有明显区别:
### 功能对比
| 特性 | /profile/:uuid | /api/profiles/minecraft |
|------|---------------|------------------------|
| **查询方式** | 单个UUID查询 | 批量用户名查询 |
| **主要用途** | 获取完整档案信息 | 批量映射用户名到UUID |
| **返回数据** | 包含纹理数据的完整信息 | 基础档案信息 |
| **使用场景** | 登录验证、档案详情展示 | 玩家列表、聊天显示 |
### 数据结构差异
`/profile/:uuid` 返回的数据包含完整的纹理信息:
```mermaid
classDiagram
class ProfileResponse {
+string uuid
+string name
+ProfileTexturesData textures
+bool is_active
+time.Time last_used_at
+time.Time created_at
}
class ProfileTexturesData {
+ProfileTexture SKIN
+ProfileTexture CAPE
}
class ProfileTexture {
+string url
+ProfileTextureMetadata metadata
}
class ProfileTextureMetadata {
+string model
}
ProfileResponse --> ProfileTexturesData : "包含"
ProfileTexturesData --> ProfileTexture : "包含"
ProfileTexture --> ProfileTextureMetadata : "包含"
```
**Diagram sources **
- [profile.go](file://internal/model/profile.go#L31-L57)
- [profile_handler.go](file://internal/handler/profile_handler.go#L164-L195)
`/api/profiles/minecraft` 返回的是基础档案信息主要用于名称到UUID的映射。
**Section sources**
- [profile.go](file://internal/model/profile.go#L31-L57)
- [profile_handler.go](file://internal/handler/profile_handler.go#L164-L195)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
## 皮肤与披风类型标识
在档案系统中,皮肤和披风的类型通过特定常量进行标识。
### 常量定义
根据 `yggdrasil_handler_test.go` 中的测试代码,皮肤和披风类型的常量定义如下:
```go
const (
TextureTypeSkin = "SKIN"
TextureTypeCape = "CAPE"
)
```
这些常量在多个地方被使用和测试,确保系统的一致性。
### 测试验证
`yggdrasil_handler_test.go` 文件中的测试函数验证了这些常量的正确性:
```mermaid
flowchart TD
Start([开始测试常量]) --> TestSkin["验证TextureTypeSkin = 'SKIN'"]
TestSkin --> TestCape["验证TextureTypeCape = 'CAPE'"]
TestCape --> CheckSkin["检查Skin常量值"]
CheckSkin --> CheckCape["检查Cape常量值"]
CheckCape --> End{测试结果}
End --> |通过| Success["测试成功"]
End --> |失败| Failure["测试失败"]
```
**Diagram sources **
- [yggdrasil_handler_test.go](file://internal/handler/yggdrasil_handler_test.go#L142-L156)
- [texture.go](file://internal/model/texture.go#L10-L13)
### 数据模型
`texture.go` 文件中,这些类型被定义为枚举类型:
```go
type TextureType string
const (
TextureTypeSkin TextureType = "SKIN"
TextureTypeCape TextureType = "CAPE"
)
```
这种设计确保了类型安全和代码可读性。
**Section sources**
- [yggdrasil_handler_test.go](file://internal/handler/yggdrasil_handler_test.go#L142-L156)
- [texture.go](file://internal/model/texture.go#L8-L13)
## 请求与响应示例
### 请求示例
```json
POST /api/profiles/minecraft
Content-Type: application/json
[
"Player1",
"Player2",
"Player3"
]
```
### 响应示例
```json
[
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"userId": 1,
"name": "Player1",
"skinId": 1,
"capeId": null,
"isActive": true,
"lastUsedAt": "2025-10-01T12:00:00Z",
"createdAt": "2025-10-01T10:00:00Z",
"updatedAt": "2025-10-01T10:00:00Z"
},
{
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"userId": 2,
"name": "Player2",
"skinId": 2,
"capeId": 3,
"isActive": true,
"lastUsedAt": "2025-10-01T11:00:00Z",
"createdAt": "2025-09-30T09:00:00Z",
"updatedAt": "2025-09-30T09:00:00Z"
}
]
```
响应包含每个玩家的UUID、用户ID、名称、皮肤ID、披风ID等信息。
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L586)
- [profile.go](file://internal/model/profile.go#L8-L24)
## 应用场景
### 游戏登录
当玩家登录游戏时,客户端会使用此服务来验证和获取玩家信息:
```mermaid
sequenceDiagram
participant Client as Minecraft客户端
participant Auth as 认证服务器
participant Profile as 档案服务
Client->>Auth : 发送登录凭证
Auth->>Profile : 查询玩家档案
Profile->>Profile : 批量查询用户名对应的UUID
Profile-->>Auth : 返回UUID和基础信息
Auth->>Client : 返回登录结果和玩家信息
```
**Diagram sources **
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
- [routes.go](file://internal/handler/routes.go#L108-L109)
### 聊天显示
在聊天系统中,当玩家发送消息时,需要显示其名称和头像:
1. 客户端获取消息中的玩家用户名
2. 调用 `/api/profiles/minecraft` 端点批量查询这些玩家的UUID
3. 使用UUID获取玩家的皮肤信息并显示头像
### 服务器列表
在多人游戏服务器列表中,需要显示在线玩家的信息:
- 使用批量查询端点获取所有在线玩家的UUID
- 根据UUID获取玩家的皮肤和披风信息
- 在服务器列表中显示玩家头像和名称
这些应用场景都依赖于高效的批量查询能力,`/api/profiles/minecraft` 端点正是为此设计。
**Section sources**
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
- [profile_service.go](file://internal/service/profile_service.go#L237-L243)

View File

@@ -1,464 +0,0 @@
# 认证服务
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go)
- [jwt.go](file://pkg/auth/jwt.go)
- [redis.go](file://pkg/redis/redis.go)
- [response.go](file://internal/model/response.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向Yggdrasil认证服务的API文档聚焦/authserver路由下的以下端点
- POST /authenticate用户凭据认证返回访问令牌与可用角色信息
- POST /validate校验访问令牌有效性
- POST /refresh在令牌过期或需要更新时刷新令牌
- POST /invalidate撤销单个访问令牌
- POST /signout基于邮箱+密码登出,撤销该用户所有令牌
文档将说明各端点的HTTP方法、请求体结构、响应数据格式、错误码含义并结合会话数据存储机制Redis与TTL设置15分钟解释authenticate如何验证用户名/邮箱与密码、validate如何检查令牌有效性、refresh如何在令牌过期后重新生成新令牌。同时给出与内部用户系统的映射逻辑如通过GetYggdrasilPasswordById查询密码
## 项目结构
Yggdrasil认证服务位于路由组“/api/v1/yggdrasil/authserver”由处理器模块负责接收请求、调用服务层完成业务逻辑并通过Redis与数据库进行会话与令牌持久化。
```mermaid
graph TB
subgraph "路由层"
R["routes.go<br/>注册/authserver路由"]
H["yggdrasil_handler.go<br/>认证处理器"]
end
subgraph "服务层"
S["yggdrasil_service.go<br/>会话与加入服务器逻辑"]
T["token_service.go<br/>令牌生成/验证/刷新/失效"]
end
subgraph "数据访问层"
YR["yggdrasil_repository.go<br/>Yggdrasil密码查询"]
end
subgraph "基础设施"
J["jwt.go<br/>JWT工具"]
RC["redis.go<br/>Redis客户端"]
RM["response.go<br/>通用响应结构"]
end
R --> H
H --> S
H --> T
S --> YR
T --> YR
H --> J
H --> RC
H --> RM
```
图表来源
- [routes.go](file://internal/handler/routes.go#L87-L111)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [response.go](file://internal/model/response.go#L1-L86)
章节来源
- [routes.go](file://internal/handler/routes.go#L87-L111)
## 核心组件
- 路由注册:在路由中将/authserver下的五个端点挂载到对应处理器函数。
- 认证处理器实现authenticate、validate、refresh、invalidate、signout等端点的请求解析、调用服务层、构造响应。
- 令牌服务:封装令牌生成、验证、刷新、失效等逻辑,维护令牌表与清理策略。
- 会话服务封装JoinServer/HasJoinedServer使用Redis存储会话数据TTL为15分钟。
- 数据访问通过repository层访问数据库如查询Yggdrasil密码。
- 基础设施JWT工具用于令牌签发与校验Redis客户端用于会话数据持久化。
章节来源
- [routes.go](file://internal/handler/routes.go#L87-L111)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [response.go](file://internal/model/response.go#L1-L86)
## 架构总览
下图展示/authserver端点的请求-处理-响应流程以及与服务层、Redis、数据库的关系。
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "yggdrasil_handler.go"
participant S as "yggdrasil_service.go"
participant T as "token_service.go"
participant YR as "yggdrasil_repository.go"
participant DB as "数据库"
participant RD as "Redis"
rect rgb(255,255,255)
Note over C,H : authenticate/validate/refresh/invalidate/signout
end
C->>H : POST /api/v1/yggdrasil/authserver/{endpoint}
H->>DB : 读取/写入数据如用户、角色、令牌
H->>RD : 读取/写入会话数据JoinServer/HasJoinedServer
H->>T : 调用令牌相关服务生成/验证/刷新/失效
H->>YR : 查询Yggdrasil密码凭据校验
T->>DB : 操作token表创建/删除/查询
S->>RD : Set/Get会话数据TTL=15分钟
H-->>C : JSON响应含状态码与数据
```
图表来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L202)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [redis.go](file://pkg/redis/redis.go#L60-L175)
## 详细组件分析
### 端点POST /authenticate
- 方法与路径
- 方法POST
- 路径:/api/v1/yggdrasil/authserver/authenticate
- 请求体结构
- agent对象描述客户端信息通常包含名称与版本
- clientToken字符串客户端令牌可选
- identifier字符串用户名或邮箱必填
- password字符串密码必填
- requestUser布尔是否返回用户属性可选
- 响应数据
- accessToken字符串访问令牌
- clientToken字符串客户端令牌
- availableProfiles数组可用角色列表每个元素为序列化后的角色信息
- selectedProfile对象当前选定的角色可选
- user对象当requestUser=true时返回包含id与properties
- 错误码
- 400请求参数错误
- 403用户名不存在或密码错误
- 500服务器内部错误
- 处理流程要点
- 根据identifier判断是邮箱还是用户名分别查询用户或角色
- 通过repository查询Yggdrasil密码并与请求密码比对
- 生成新令牌包含accessToken、clientToken并返回可用角色列表
- 如requestUser为true附加用户属性
- 与内部用户系统映射
- 通过GetYggdrasilPasswordById查询Yggdrasil密码并与请求密码比对
- 通过GetUserIDByEmail或GetProfileByProfileName获取用户ID
- 通过GetProfileByUserId获取角色列表自动选择唯一角色
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "Authenticate()"
participant DB as "数据库"
participant YR as "yggdrasil_repository.go"
participant T as "token_service.go"
C->>H : JSON请求identifier/password等
H->>DB : 根据identifier定位用户/角色
H->>YR : GetYggdrasilPasswordById(userId)
YR-->>H : 返回Yggdrasil密码
H->>H : 校验密码
H->>T : NewToken(userId, UUID, clientToken)
T-->>H : 返回accessToken/clientToken/availableProfiles
H-->>C : 200 JSONaccessToken/clientToken/availableProfiles/selectedProfile/user
```
图表来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L246)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L79)
- [token_service.go](file://internal/service/token_service.go#L24-L79)
章节来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L246)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L79)
- [token_service.go](file://internal/service/token_service.go#L24-L79)
### 端点POST /validate
- 方法与路径
- 方法POST
- 路径:/api/v1/yggdrasil/authserver/validate
- 请求体结构
- accessToken字符串访问令牌必填
- clientToken字符串客户端令牌可选
- 响应数据
- 当令牌有效204 No Content无body
- 当令牌无效403 Forbiddenvalid=false
- 处理流程要点
- 调用ValidToken校验accessToken与clientToken若提供
- 有效返回204无效返回403
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "ValidToken()"
participant T as "token_service.go"
C->>H : JSON请求accessToken, clientToken
H->>T : ValidToken(accessToken, clientToken)
alt 有效
H-->>C : 204 No Content
else 无效
H-->>C : 403 {valid : false}
end
```
图表来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L248-L267)
- [token_service.go](file://internal/service/token_service.go#L116-L141)
章节来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L248-L267)
- [token_service.go](file://internal/service/token_service.go#L116-L141)
### 端点POST /refresh
- 方法与路径
- 方法POST
- 路径:/api/v1/yggdrasil/authserver/refresh
- 请求体结构
- accessToken字符串访问令牌必填
- clientToken字符串客户端令牌可选
- requestUser布尔是否返回用户属性可选
- selectedProfile对象包含id字段可选
- 响应数据
- accessToken字符串新的访问令牌
- clientToken字符串客户端令牌
- selectedProfile对象选定的角色可选
- user对象当requestUser=true时返回包含id与properties
- 处理流程要点
- 通过accessToken获取UUID与用户ID
- 校验selectedProfile若提供与用户匹配
- 调用RefreshToken生成新令牌删除旧令牌
- 返回新令牌与可选数据
- 与内部用户系统映射
- 通过GetUUIDByAccessToken与GetUserIDByAccessToken获取用户与角色信息
- 通过ValidateProfileByUserID校验角色归属
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "RefreshToken()"
participant T as "token_service.go"
participant DB as "数据库"
C->>H : JSON请求accessToken, clientToken, selectedProfile, requestUser
H->>DB : GetUUIDByAccessToken / GetUserIDByAccessToken
H->>H : 校验selectedProfile归属
H->>T : RefreshToken(oldAccessToken, clientToken, selectedProfileID)
T-->>H : 返回newAccessToken, clientToken
H-->>C : 200 JSONnewAccessToken, clientToken, selectedProfile, user
```
图表来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L269-L361)
- [token_service.go](file://internal/service/token_service.go#L151-L238)
章节来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L269-L361)
- [token_service.go](file://internal/service/token_service.go#L151-L238)
### 端点POST /invalidate
- 方法与路径
- 方法POST
- 路径:/api/v1/yggdrasil/authserver/invalidate
- 请求体结构
- accessToken字符串访问令牌必填
- clientToken字符串客户端令牌可选
- 响应数据
- 204 No Content
- 处理流程要点
- 调用InvalidToken删除对应accessToken
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "InvalidToken()"
participant T as "token_service.go"
C->>H : JSON请求accessToken, clientToken
H->>T : InvalidToken(accessToken)
T-->>H : 删除完成
H-->>C : 204 No Content
```
图表来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L363-L378)
- [token_service.go](file://internal/service/token_service.go#L240-L257)
章节来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L363-L378)
- [token_service.go](file://internal/service/token_service.go#L240-L257)
### 端点POST /signout
- 方法与路径
- 方法POST
- 路径:/api/v1/yggdrasil/authserver/signout
- 请求体结构
- username字符串邮箱必填
- password字符串密码必填
- 响应数据
- 204 No Content
- 处理流程要点
- 校验邮箱格式
- 通过邮箱获取用户并查询Yggdrasil密码
- 对比请求密码与存储密码
- 调用InvalidUserTokens撤销该用户所有令牌
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "SignOut()"
participant DB as "数据库"
participant T as "token_service.go"
C->>H : JSON请求username, password
H->>H : 校验邮箱格式
H->>DB : GetUserByEmail / GetYggdrasilPasswordById
H->>H : 校验密码
H->>T : InvalidUserTokens(userId)
T-->>H : 删除完成
H-->>C : 204 No Content
```
图表来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L380-L425)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [token_service.go](file://internal/service/token_service.go#L259-L277)
章节来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L380-L425)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [token_service.go](file://internal/service/token_service.go#L259-L277)
### 会话数据存储与TTLJoinServer/HasJoinedServer
- 存储机制
- 使用Redis存储玩家加入服务器的会话数据键规则为“Join_”前缀+serverId
- 数据结构包含accessToken、userName、selectedProfile、ip
- TTL设置
- 会话数据TTL为15分钟
- 读写流程
- JoinServer校验参数与Token获取角色信息序列化会话数据并写入Redis
- HasJoinedServer从Redis读取会话数据反序列化后校验用户名与IP可选
```mermaid
flowchart TD
Start(["进入JoinServer"]) --> Validate["校验serverId/accessToken/selectedProfile非空"]
Validate --> Format["格式化UUID与IP校验"]
Format --> GetToken["根据accessToken查询Token"]
GetToken --> MatchProfile["selectedProfile与Token绑定Profile匹配"]
MatchProfile --> LoadProfile["加载角色信息"]
LoadProfile --> BuildData["构建SessionData结构"]
BuildData --> Marshal["序列化SessionData"]
Marshal --> Store["Redis Set(key='Join_serverId', value, TTL=15m)"]
Store --> End(["完成"])
subgraph "HasJoinedServer"
HStart["读取serverId/username"] --> HLoad["Redis Get('Join_serverId')"]
HLoad --> Parse["反序列化SessionData"]
Parse --> CheckUser["校验userName匹配"]
CheckUser --> CheckIP{"提供IP?"}
CheckIP --> |是| IPMatch["校验IP匹配"]
CheckIP --> |否| Success["通过"]
IPMatch --> Success
end
```
图表来源
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L202)
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go#L1-L351)
- [redis.go](file://pkg/redis/redis.go#L60-L175)
章节来源
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L19-L31)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L202)
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go#L1-L351)
- [redis.go](file://pkg/redis/redis.go#L60-L175)
## 依赖关系分析
- 路由到处理器:/api/v1/yggdrasil/authserver/* 映射到yggdrasil_handler.go中的对应函数
- 处理器到服务:认证处理器调用令牌服务与会话服务
- 服务到仓库令牌服务与会话服务通过repository层访问数据库
- 会话服务到RedisJoinServer/HasJoinedServer直接使用Redis客户端
- 响应结构统一使用通用响应结构参考response.go
```mermaid
graph LR
Routes["routes.go"] --> Handler["yggdrasil_handler.go"]
Handler --> TokenSvc["token_service.go"]
Handler --> YggSvc["yggdrasil_service.go"]
YggSvc --> Redis["redis.go"]
TokenSvc --> Repo["yggdrasil_repository.go"]
Handler --> Resp["response.go"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L87-L111)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [response.go](file://internal/model/response.go#L1-L86)
章节来源
- [routes.go](file://internal/handler/routes.go#L87-L111)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [response.go](file://internal/model/response.go#L1-L86)
## 性能考量
- Redis读写JoinServer/HasJoinedServer使用GetBytes/SetTTL为15分钟适合短生命周期的会话数据
- 令牌清理NewToken后异步触发CheckAndCleanupExcessTokens限制用户最多保留10个令牌降低数据库压力
- 超时控制:服务层对数据库查询设置默认超时,避免阻塞
- 并发安全:刷新令牌采用先创建新令牌再删除旧令牌的双写策略,减少事务复杂度
章节来源
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L202)
- [token_service.go](file://internal/service/token_service.go#L81-L114)
- [redis.go](file://pkg/redis/redis.go#L60-L175)
## 故障排查指南
- 400 Bad Request
- 请求体解析失败或参数缺失
- 会话加入/验证参数缺失serverId/username
- 403 Forbidden
- 认证失败(用户名不存在或密码错误)
- 令牌无效或clientToken不匹配
- 用户与角色不匹配
- 500 Internal Server Error
- 生成令牌失败、读取/写入Redis失败、数据库查询异常
- 常见问题定位
- 确认identifier是否为邮箱或用户名
- 确认clientToken是否与accessToken匹配validate/refresh
- 检查Redis连接与TTL设置JoinServer/HasJoinedServer
- 检查用户角色与selectedProfile是否匹配refresh
章节来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L202)
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go#L1-L351)
## 结论
本认证服务围绕/authserver路由提供了完整的Yggdrasil认证能力覆盖凭据认证、令牌验证、刷新、失效与登出。通过Redis实现15分钟TTL的会话数据存储结合令牌服务的清理策略与严格的参数校验确保系统在安全性与性能之间取得平衡。与内部用户系统的映射清晰凭据校验通过getYggdrasilPasswordById完成角色管理与令牌绑定完善。
## 附录
- 错误码对照
- 400请求参数错误
- 403权限不足/令牌无效/用户不匹配
- 500服务器内部错误
- 响应结构
- 通用响应结构参考response.go中的Response/Error结构
- 会话数据结构
- SessionData包含accessToken、userName、selectedProfile、ip
章节来源
- [response.go](file://internal/model/response.go#L1-L86)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L19-L31)

View File

@@ -1,284 +0,0 @@
# 分页处理机制
<cite>
**本文引用的文件**
- [texture_service_test.go](file://internal/service/texture_service_test.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [response.go](file://internal/model/response.go)
- [common.go](file://internal/types/common.go)
- [routes.go](file://internal/handler/routes.go)
- [texture.go](file://internal/model/texture.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本文件围绕材质Texture相关API的分页处理机制进行系统化梳理重点基于测试用例与实现代码阐明以下内容
- 分页参数的边界处理规则page小于1时自动设为1pageSize小于1或大于100时自动设为20。
- repository层如何通过Offset与Limit实现分页查询并说明Preload('Uploader')对查询性能的影响。
- 分页响应中包含总数total的设计目的与客户端使用建议。
- 分页查询的性能优化策略,包括索引使用与大数据量下的性能考虑。
## 项目结构
与分页处理直接相关的模块分布如下:
- Handler层负责接收HTTP请求、解析分页参数、调用服务层并构造分页响应。
- Service层对分页参数进行边界校正然后委托repository层执行查询。
- Repository层使用GORM构建查询统计总数并按Offset/Limit分页必要时Preload关联对象。
- Model层定义实体及索引为分页查询提供索引支持。
- Response模型统一分页响应结构包含total、page、per_page等字段。
```mermaid
graph TB
subgraph "HTTP层"
R["routes.go<br/>注册路由"]
H["texture_handler.go<br/>处理GET /texture/*"]
end
subgraph "服务层"
S["texture_service.go<br/>边界校正与调用仓库"]
end
subgraph "仓库层"
REPO["texture_repository.go<br/>Offset/Limit分页与Preload"]
end
subgraph "模型层"
M["texture.go<br/>实体与索引"]
end
subgraph "响应模型"
RESP["response.go<br/>PaginationResponse(total/page/per_page)"]
end
R --> H
H --> S
S --> REPO
REPO --> M
H --> RESP
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
## 核心组件
- 分页参数边界处理在服务层对page与pageSize进行强制校正确保合法范围。
- Offset/Limit分页仓库层使用Offset与Limit执行分页查询并在需要时Preload关联对象。
- 分页响应结构统一的PaginationResponse包含total、page、per_page便于前端计算总页数与导航。
- 索引设计:模型层定义了多处索引,有助于提升分页查询与过滤性能。
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [response.go](file://internal/model/response.go#L10-L18)
- [texture.go](file://internal/model/texture.go#L16-L36)
## 架构总览
下图展示了从HTTP请求到分页响应的关键流程以及各层之间的职责划分。
```mermaid
sequenceDiagram
participant C as "客户端"
participant G as "Gin路由(routes.go)"
participant H as "纹理处理器(texture_handler.go)"
participant S as "纹理服务(texture_service.go)"
participant R as "纹理仓库(texture_repository.go)"
participant DB as "数据库(GORM)"
C->>G : "GET /api/v1/texture/my?page=...&page_size=..."
G->>H : "转发到 GetUserTextures"
H->>H : "解析page/page_size(默认1/20)"
H->>S : "GetUserTextures(userID, page, page_size)"
S->>S : "边界校正 : page>=1, 1<=page_size<=100"
S->>R : "FindTexturesByUploaderID(uploaderID, page, pageSize)"
R->>DB : "Count统计总数"
R->>DB : "Preload('Uploader') + Order + Offset + Limit"
DB-->>R : "结果集与总数"
R-->>S : "[]Texture, total"
S-->>H : "[]Texture, total"
H->>H : "转换为TextureInfo切片"
H-->>C : "PaginationResponse{data, total, page, per_page}"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L535)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
## 详细组件分析
### 分页参数边界处理规则
- page边界当page小于1时自动修正为1。
- pageSize边界当pageSize小于1或大于100时自动修正为20。
- 适用范围:服务层对“我的材质”、“搜索材质”、“我的收藏”三类接口均执行上述校正。
```mermaid
flowchart TD
Start(["进入服务层分页处理"]) --> CheckPage["校验 page < 1 ?"]
CheckPage --> |是| FixPage["page = 1"]
CheckPage --> |否| KeepPage["保持原值"]
FixPage --> CheckSize["校验 pageSize < 1 或 > 100 ?"]
KeepPage --> CheckSize
CheckSize --> |是| FixSize["pageSize = 20"]
CheckSize --> |否| KeepSize["保持原值"]
FixSize --> End(["返回仓库层查询"])
KeepSize --> End
```
图表来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L102-L161)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L102-L161)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)
### repository层的Offset与Limit实现
- 统计总数先以相同过滤条件执行Count得到total。
- 分页查询计算offset=(page-1)*pageSize随后使用Order排序、Offset与Limit分页并在需要时Preload('Uploader')加载关联用户信息。
- 查询范围控制:
- “我的材质”按uploader_id且status!= -1过滤。
- “搜索材质”按status=1过滤可叠加public_only、type、关键词LIKE。
- “我的收藏”通过子查询获取收藏的texture_id再按status=1过滤。
```mermaid
flowchart TD
QStart["开始查询"] --> BuildQuery["构建基础查询(过滤条件)"]
BuildQuery --> CountTotal["Count统计总数(total)"]
CountTotal --> CalcOffset["计算 offset = (page-1)*pageSize"]
CalcOffset --> Preload["必要时 Preload('Uploader')"]
Preload --> Order["Order 排序(如created_at DESC)"]
Order --> ApplyLimit["Offset + Limit 分页"]
ApplyLimit --> ExecFind["执行查询并返回结果集"]
ExecFind --> Done["返回 []Texture, total"]
```
图表来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### Preload('Uploader')对查询性能的影响
- 优点避免N+1查询问题一次性加载每个材质的Uploader信息减少额外查询次数。
- 潜在代价当分页结果较大时会增加单次查询的数据量与网络传输开销同时JOIN或Preload可能带来额外的内存与CPU消耗。
- 建议在高频分页场景中若Uploader信息非必须可考虑延迟加载或仅在详情页Preload若必须显示作者信息则可在业务上控制pageSize上限平衡性能与体验。
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### 分页响应中的总数total设计目的与客户端使用建议
- 设计目的:
- 提供准确的总量信息便于前端计算总页数与展示“共X条”等提示。
- 协助客户端实现“无限滚动”或“上拉加载”的边界判断。
- 客户端使用建议:
- 使用 total 与 per_page 计算 total_pagestotal_pages = ceil(total / per_page)。
- 在首次加载后缓存 total避免重复Count带来的性能损耗。
- 若total变化频繁可采用“乐观更新”策略在新增/删除后局部更新列表与total而非全量刷新。
章节来源
- [response.go](file://internal/model/response.go#L10-L18)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L535)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
### 分页查询的性能优化策略
- 索引使用:
- textures表的多处索引有助于过滤与排序如uploader_id、is_public、hash、download_count、favorite_count、status等。
- 搜索场景建议对name/description建立全文索引或使用更高效的检索方案如向量检索以降低LIKE模糊匹配成本。
- 大数据量下的考虑:
- 控制pageSize上限服务层默认20最大100避免单页过大导致内存与网络压力。
- 使用覆盖索引与选择性高的过滤条件优先,减少扫描范围。
- 对频繁访问的列表页可引入缓存如Redis存储热门查询结果与total结合失效策略保证一致性。
- 对Preload('Uploader')若非必须可延迟加载或按需加载减少不必要的JOIN与数据传输。
章节来源
- [texture.go](file://internal/model/texture.go#L16-L36)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
## 依赖分析
- Handler层依赖Service层提供的分页接口将HTTP参数转换为服务层输入并构造统一的分页响应。
- Service层依赖Repository层执行数据库查询并在必要时进行参数边界校正。
- Repository层依赖Model层的实体定义与索引使用GORM执行Count、Offset/Limit与Preload。
- Response模型提供统一的分页响应结构便于前后端约定。
```mermaid
graph LR
Handler["texture_handler.go"] --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> Model["texture.go"]
Handler --> Resp["response.go"]
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
## 性能考量
- 参数校正与分页服务层的边界校正与仓库层的Offset/Limit组合确保查询稳定可控。
- Preload策略在高频列表页中谨慎使用Preload('Uploader'),必要时采用延迟加载或缓存。
- 索引与过滤:利用现有索引减少全表扫描;对搜索关键词建立高效索引或采用替代检索方案。
- 缓存与限流:对热门列表页引入缓存与限流,降低数据库压力。
- 分页上限服务层默认pageSize为20最大100有助于控制单次查询负载。
[本节为通用性能指导,不直接分析具体文件]
## 故障排查指南
- 常见问题与定位思路:
- 分页参数异常确认page与pageSize是否被正确校正小于1设为1超出范围设为默认值
- total与实际条目不符检查过滤条件是否一致如status!= -1、public_only=true等
- 查询缓慢检查是否命中索引、是否使用了不必要的Preload、是否超大pageSize。
- N+1查询确认是否在循环中访问Uploader避免多次查询。
- 相关实现参考路径:
- 分页参数边界处理:[texture_service.go](file://internal/service/texture_service.go#L81-L103)
- 分页查询与总数统计:[texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- 分页响应结构:[response.go](file://internal/model/response.go#L10-L18)
- HTTP层参数解析与响应构造[texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [response.go](file://internal/model/response.go#L10-L18)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
## 结论
本项目的材质相关API分页处理遵循清晰的边界校正与统一响应规范。服务层负责参数合法性保障仓库层通过Count+Offset/Limit实现高效分页并在必要时Preload关联对象。分页响应中的total为前端提供了可靠的分页计算依据。结合现有索引与合理的pageSize上限系统在大多数场景下能够稳定运行。针对高频列表页建议进一步引入缓存与按需加载策略以应对更大规模的数据与更高的并发需求。

View File

@@ -1,431 +0,0 @@
# 材质API
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [common.go](file://internal/types/common.go)
- [upload_service.go](file://internal/service/upload_service.go)
- [minio.go](file://pkg/storage/minio.go)
- [auth.go](file://internal/middleware/auth.go)
- [texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向“材质管理API”的使用者与维护者系统性梳理材质的搜索、上传、创建、更新、删除、收藏及分页查询能力。基于路由组 /api/v1/texture 的公开与认证两类访问模式详细说明各端点的行为、参数、响应与错误处理并深入解析生成上传URL的流程、材质元数据创建、分页查询策略以及收藏功能的实现细节。同时解释材质与用户的关系及权限控制机制帮助读者快速上手并正确集成。
## 项目结构
- 路由注册集中在路由处理器中,按组划分公开与认证两类接口。
- 处理器负责参数校验、鉴权、调用服务层并返回统一响应。
- 服务层封装业务规则(如权限校验、分页边界、收藏增删计数)。
- 仓储层负责数据库查询与写入(含软删除、计数更新、收藏关联)。
- 模型层定义材质、收藏、下载日志等实体及其索引。
- 上传服务与存储客户端负责生成预签名上传URL与对象命名策略。
- 中间件提供JWT认证与可选认证能力。
```mermaid
graph TB
subgraph "HTTP层"
R["路由注册<br/>routes.go"]
H["处理器<br/>texture_handler.go"]
end
subgraph "服务层"
S["服务<br/>texture_service.go"]
end
subgraph "仓储层"
REPO["仓储<br/>texture_repository.go"]
end
subgraph "模型层"
M["模型<br/>texture.go"]
end
subgraph "上传与存储"
U["上传服务<br/>upload_service.go"]
ST["存储客户端<br/>minio.go"]
end
subgraph "鉴权"
A["认证中间件<br/>auth.go"]
end
R --> H
H --> A
H --> S
S --> REPO
REPO --> M
H --> U
U --> ST
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
## 核心组件
- 路由与鉴权
- 公开端点GET /api/v1/texture、GET /api/v1/texture/{id}
- 认证端点POST /api/v1/texture/upload-url、POST /api/v1/texture、PUT /api/v1/texture/{id}、DELETE /api/v1/texture/{id}、POST /api/v1/texture/{id}/favorite、GET /api/v1/texture/my、GET /api/v1/texture/favorites
- 认证中间件要求 Authorization: Bearer <token>,通过后将用户信息注入上下文
- 数据模型
- 材质实体包含上传者ID、名称、描述、类型、URL、哈希、大小、公开状态、下载/收藏计数、是否细臂、状态、时间戳等;并关联上传者
- 收藏关联表 user_texture_favorites 记录用户与材质的收藏关系
- 下载日志表 texture_download_logs 记录下载行为
- 上传与存储
- 生成预签名POST URL限定文件类型、大小范围与过期时间对象路径按用户与材质类型组织
- 服务与仓储
- 提供搜索、分页、权限校验、收藏切换、软删除、计数更新等业务逻辑
- 仓储实现分页查询、总数统计、软删除、收藏增删与计数更新
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
## 架构总览
下图展示从HTTP请求到数据库与存储的关键交互路径涵盖公开搜索、认证上传与创建、权限校验、收藏与分页等。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由<br/>routes.go"
participant H as "处理器<br/>texture_handler.go"
participant A as "认证中间件<br/>auth.go"
participant S as "服务<br/>texture_service.go"
participant REPO as "仓储<br/>texture_repository.go"
participant M as "模型<br/>texture.go"
participant U as "上传服务<br/>upload_service.go"
participant ST as "存储客户端<br/>minio.go"
C->>R : 请求 /api/v1/texture/search
R->>H : 调用 SearchTextures
H->>S : 调用 SearchTextures(db, keyword, type, publicOnly, page, pageSize)
S->>REPO : 查询并统计总数
REPO-->>S : 返回列表与总数
S-->>H : 返回结果
H-->>C : 200 + 分页数据
C->>R : 请求 /api/v1/texture/upload-url (认证)
R->>A : 鉴权
A-->>R : 注入用户ID
R->>H : 调用 GenerateTextureUploadURL
H->>U : 生成预签名POST URL
U->>ST : 生成POST策略与URL
ST-->>U : 返回PostURL、FormData、FileURL
U-->>H : 返回结果
H-->>C : 200 + 上传URL与过期时间
C->>R : 请求 /api/v1/texture (认证)
R->>A : 鉴权
A-->>R : 注入用户ID
R->>H : 调用 CreateTexture
H->>S : 检查上传配额与创建材质
S->>REPO : 写入材质记录
REPO-->>S : 返回新记录
S-->>H : 返回材质信息
H-->>C : 200 + 材质详情
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [minio.go](file://pkg/storage/minio.go#L82-L121)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [texture.go](file://internal/model/texture.go#L1-L77)
## 详细组件分析
### 路由与访问模式
- 公开访问
- GET /api/v1/texture搜索材质关键词、类型、公开筛选、分页
- GET /api/v1/texture/{id}:获取材质详情
- 认证访问
- POST /api/v1/texture/upload-url生成材质上传URL预签名POST
- POST /api/v1/texture创建材质记录文件已上传至存储
- PUT /api/v1/texture/{id}:更新材质(仅上传者可操作)
- DELETE /api/v1/texture/{id}:删除材质(软删除,仅上传者可操作)
- POST /api/v1/texture/{id}/favorite切换收藏
- GET /api/v1/texture/my获取当前用户上传的材质列表分页
- GET /api/v1/texture/favorites获取当前用户收藏的材质列表分页
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
### 生成上传URL流程
- 客户端向 POST /api/v1/texture/upload-url 发起请求携带文件名与材质类型SKIN/CAPE
- 处理器校验请求体、获取用户ID并调用上传服务
- 上传服务:
- 校验文件名扩展名与类型
- 选择对应上传配置(大小范围、过期时间)
- 解析存储桶名称textures
- 生成对象名user_{userID}/{textureTypeFolder}/{timestamp}_{originalFileName}
- 生成预签名POST策略含内容长度范围、过期时间返回PostURL、FormData与最终访问URL
- 客户端使用返回的PostURL与FormData直传到对象存储成功后调用 POST /api/v1/texture 创建材质记录
```mermaid
flowchart TD
Start(["开始"]) --> Bind["绑定请求体<br/>文件名/类型"]
Bind --> Validate["校验文件名与类型"]
Validate --> Config["加载上传配置"]
Config --> Bucket["解析存储桶名称"]
Bucket --> ObjName["生成对象名<br/>user_{id}/{type}/{ts}_{fileName}"]
ObjName --> Policy["生成预签名POST策略"]
Policy --> Result["返回PostURL/FormData/FileURL"]
Result --> End(["结束"])
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [minio.go](file://pkg/storage/minio.go#L82-L121)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [minio.go](file://pkg/storage/minio.go#L82-L121)
### 材质元数据创建
- 客户端上传完成后,向 POST /api/v1/texture 提交材质元数据名称、描述、类型、URL、哈希、大小、公开状态、是否细臂
- 处理器:
- 校验请求体
- 检查用户上传配额默认最大100条
- 调用服务层创建材质记录(校验用户存在、去重哈希、转换类型、初始化默认值)
- 写入数据库并返回材质信息
- 服务层:
- 校验用户存在
- 哈希去重检查
- 类型转换SKIN/CAPE
- 初始化状态、下载/收藏计数为0
- 仓储层:
- 插入记录
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [texture_repository.go](file://internal/repository/texture_repository.go#L9-L13)
- [texture.go](file://internal/model/texture.go#L16-L35)
### 搜索与分页查询
- GET /api/v1/texture 支持:
- keyword关键词名称/描述模糊匹配)
- typeSKIN/CAPE
- public_only仅公开材质
- page/page_size分页最小1最大100默认20
- 服务层对分页参数进行边界修正,仓储层:
- 若 public_only 为真,则过滤 is_public=true
- 按 type 进行精确过滤
- 按 name/description 模糊匹配
- 统计总数并按创建时间倒序分页查询
- 返回统一分页响应list、total、page、page_size、total_pages
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
### 更新与删除
- PUT /api/v1/texture/{id}
- 仅材质上传者可更新
- 支持更新名称、描述、公开状态(可选字段)
- 服务层按非空字段构建更新集合并执行
- DELETE /api/v1/texture/{id}
- 仅材质上传者可删除
- 采用软删除status=-1不影响历史下载/收藏计数
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L419)
- [texture_service.go](file://internal/service/texture_service.go#L105-L160)
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L131)
### 收藏功能
- POST /api/v1/texture/{id}/favorite
- 切换收藏状态(已收藏则取消,未收藏则添加)
- 服务层先检查材质是否存在,再判断是否已收藏
- 成功后分别更新 user_texture_favorites 与 textures.favorite_count
- 返回 is_favorited 结果
- GET /api/v1/texture/favorites
- 获取当前用户收藏的材质列表分页最小1最大100默认20
- 仓储层通过子查询定位收藏的材质ID再查询并统计总数
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L189-L238)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [texture.go](file://internal/model/texture.go#L42-L57)
### 我的材质
- GET /api/v1/texture/my
- 获取当前用户上传的材质列表分页最小1最大100默认20
- 仓储层按 uploader_id 且 status!=-1 过滤,统计总数并分页查询
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L535)
- [texture_service.go](file://internal/service/texture_service.go#L81-L91)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
### 权限控制与用户关系
- 认证中间件要求 Authorization: Bearer <token>并将用户ID、用户名、角色注入上下文
- 权限规则:
- 仅上传者可更新/删除材质
- 仅登录用户可收藏/取消收藏
- 搜索公开材质时可匿名访问
- 上传URL生成与创建材质需登录
- 材质与用户:
- 材质实体包含 uploader_id 字段,指向上传者
- 模型中定义了 Uploader 关联,便于返回上传者信息
章节来源
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L419)
- [texture.go](file://internal/model/texture.go#L16-L35)
## 依赖分析
- 处理器依赖:
- 类型定义:请求/响应结构体(来自 internal/types/common.go
- 服务层:业务逻辑封装
- 存储服务生成预签名URL
- 服务层依赖:
- 仓储层:数据库操作
- 模型层:实体定义
- 仓储层依赖:
- 数据库连接与GORM
- 模型层:实体与索引
- 上传服务依赖:
- 存储客户端生成POST策略与URL
- 配置上传大小范围、过期时间、SSL与Endpoint
```mermaid
graph LR
H["texture_handler.go"] --> T["types/common.go"]
H --> S["texture_service.go"]
S --> REPO["texture_repository.go"]
REPO --> M["texture.go"]
H --> U["upload_service.go"]
U --> ST["minio.go"]
H --> A["auth.go"]
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
- [common.go](file://internal/types/common.go#L86-L191)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
## 性能考虑
- 分页参数边界
- 所有分页接口均对 page/page_size 进行边界约束最小1最大100默认20避免过大请求导致数据库压力
- 查询优化
- 搜索接口按 status=1 与可选 is_public、type、关键词进行过滤建议在相关列建立索引以提升查询效率
- 分页查询按 created_at 倒序,结合索引可减少排序成本
- 计数更新
- 收藏/下载计数采用原子更新UpdateColumn降低并发冲突概率
- 上传URL
- 预签名POST策略限制文件大小范围与过期时间减少无效请求与存储压力
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
## 故障排查指南
- 认证失败
- 缺少Authorization头或格式不正确返回401
- Token无效返回401
- 参数错误
- 请求体绑定失败返回400
- 无效的材质ID返回400
- 权限不足
- 非上传者尝试更新/删除返回403
- 业务异常
- 材质不存在返回404
- 已达到上传数量上限返回400
- 哈希重复返回400
- 上传URL生成失败
- 文件名不合法、类型不支持、存储桶不存在、生成策略失败返回400
- 搜索/分页异常
- 数据库查询失败返回500
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L419)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L599)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [texture_service.go](file://internal/service/texture_service.go#L105-L160)
- [texture_service.go](file://internal/service/texture_service.go#L189-L238)
## 结论
材质API围绕公开搜索与认证上传两条主线设计通过严格的参数校验、权限控制与分页策略保障可用性与性能。上传流程采用预签名POST策略既简化客户端实现又保证安全性。收藏与分页查询进一步完善了用户体验。建议在生产环境中为常用查询列建立索引并结合监控与日志持续优化性能与稳定性。
## 附录
### API端点一览公开与认证
- 公开
- GET /api/v1/texture搜索材质关键词、类型、公开筛选、分页
- GET /api/v1/texture/{id}:获取材质详情
- 认证
- POST /api/v1/texture/upload-url生成材质上传URL预签名POST
- POST /api/v1/texture创建材质记录
- PUT /api/v1/texture/{id}:更新材质
- DELETE /api/v1/texture/{id}:删除材质
- POST /api/v1/texture/{id}/favorite切换收藏
- GET /api/v1/texture/my我的材质分页
- GET /api/v1/texture/favorites我的收藏分页
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
### 请求/响应要点
- 生成上传URL
- 请求体file_name、texture_typeSKIN/CAPE
- 响应体post_url、form_data、texture_url、expires_in
- 创建材质
- 请求体name、description、type、url、hash、size、is_public、is_slim
- 响应体:完整材质信息(含计数与时间戳)
- 搜索
- 查询参数keyword、type、public_only、page、page_size
- 响应体:分页列表与总数
- 收藏
- 请求体:无
- 响应体is_favorited
- 我的材质/收藏
- 查询参数page、page_size
- 响应体:分页列表与总数
章节来源
- [common.go](file://internal/types/common.go#L86-L191)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L599)

View File

@@ -1,145 +0,0 @@
# 材质上传与管理
<cite>
**本文档引用的文件**
- [texture_service.go](file://internal/service/texture_service.go)
- [upload_service.go](file://internal/service/upload_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [routes.go](file://internal/handler/routes.go)
</cite>
## 目录
1. [简介](#简介)
2. [上传流程与元数据创建](#上传流程与元数据创建)
3. [上传限制检查机制](#上传限制检查机制)
4. [材质更新API权限控制](#材质更新api权限控制)
5. [字段更新策略](#字段更新策略)
6. [错误处理场景](#错误处理场景)
## 简介
本系统提供完整的材质上传与管理功能支持用户上传皮肤SKIN和披风CAPE材质。系统通过预签名URL实现安全的文件上传确保只有经过身份验证的用户才能上传和管理自己的材质。材质元数据存储在数据库中包含名称、描述、类型、哈希值等信息。系统实现了严格的权限控制确保只有上传者才能修改或删除自己的材质。同时系统还提供了收藏、下载统计等功能增强了用户体验。
## 上传流程与元数据创建
材质上传流程分为两个阶段生成上传URL和创建材质元数据。首先用户通过调用`/api/v1/texture/upload-url`接口获取预签名的上传URL。该接口验证用户身份、文件名和材质类型后生成一个临时的上传链接允许用户直接上传文件到存储服务。上传完成后用户调用`/api/v1/texture`接口创建材质元数据。系统会验证用户是否存在、材质哈希是否重复并创建相应的数据库记录。材质元数据包括上传者ID、名称、描述、类型、URL、哈希值、大小、公开状态等信息。
```mermaid
sequenceDiagram
participant 用户 as 用户
participant 处理器 as texture_handler
participant 服务 as upload_service
participant 存储 as 存储服务
用户->>处理器 : POST /api/v1/texture/upload-url
处理器->>服务 : GenerateTextureUploadURL()
服务->>服务 : 验证文件名和类型
服务->>存储 : 生成预签名POST URL
存储-->>服务 : 返回预签名URL
服务-->>处理器 : 返回URL
处理器-->>用户 : 返回上传URL
用户->>存储 : 使用预签名URL上传文件
存储-->>用户 : 上传成功
用户->>处理器 : POST /api/v1/texture
处理器->>服务 : CreateTexture()
服务->>服务 : 验证用户和哈希
服务->>repository : CreateTexture()
repository-->>服务 : 创建成功
服务-->>处理器 : 返回材质信息
处理器-->>用户 : 返回创建结果
```
**图示来源**
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [texture_service.go](file://internal/service/texture_service.go#L12-L63)
- [routes.go](file://internal/handler/routes.go#L53-L54)
**本节来源**
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [texture_service.go](file://internal/service/texture_service.go#L12-L63)
## 上传限制检查机制
系统通过`CheckTextureUploadLimit`函数实现用户上传数量限制。该函数接收数据库连接、上传者ID和最大允许上传数量作为参数。首先函数调用`CountTexturesByUploaderID`从数据库中统计指定用户已上传的材质数量。然后将统计结果与最大限制进行比较。如果当前上传数量大于或等于最大限制函数返回错误信息提示用户已达到最大上传数量限制。否则函数返回nil表示可以继续上传。系统配置中定义了每个用户的最大材质数量限制默认值为100。
```mermaid
flowchart TD
Start([开始]) --> Count["调用CountTexturesByUploaderID<br/>统计用户上传数量"]
Count --> Compare{"当前数量 >= 最大限制?"}
Compare --> |是| ReturnError["返回错误信息:<br/>已达到最大上传数量限制"]
Compare --> |否| ReturnSuccess["返回nil允许上传"]
ReturnError --> End([结束])
ReturnSuccess --> End
```
**图示来源**
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
- [texture_repository.go](file://internal/repository/texture_repository.go#L223-L231)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
## 材质更新API权限控制
材质更新API实现了严格的权限控制确保只有上传者才能修改自己的材质。当用户尝试更新材质时系统首先通过`FindTextureByID`从数据库中获取材质信息。然后比较请求中的上传者ID与材质记录中的`UploaderID`。如果两者不匹配,系统返回"无权修改此材质"的错误信息。这种基于上传者ID的权限检查机制有效防止了未经授权的修改操作。权限检查在`UpdateTexture`服务函数中实现,是更新操作的第一步,确保在进行任何数据修改之前完成身份验证。
```mermaid
flowchart TD
Start([开始]) --> GetTexture["调用FindTextureByID<br/>获取材质信息"]
GetTexture --> CheckExist{"材质存在?"}
CheckExist --> |否| ReturnError1["返回错误:<br/>材质不存在"]
CheckExist --> |是| CheckPermission{"UploaderID匹配?"}
CheckPermission --> |否| ReturnError2["返回错误:<br/>无权修改此材质"]
CheckPermission --> |是| UpdateFields["更新指定字段"]
UpdateFields --> ReturnSuccess["返回更新后的材质"]
ReturnError1 --> End([结束])
ReturnError2 --> End
ReturnSuccess --> End
```
**图示来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L16-L27)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
## 字段更新策略
系统采用灵活的字段更新策略,仅更新客户端提供的字段。当更新材质时,系统创建一个`updates`映射来存储需要更新的字段。如果提供了名称且不为空,将其添加到`updates`映射中;如果提供了描述且不为空,也将其添加到映射中;如果提供了公开状态(`isPublic`),同样将其添加到映射中。只有当`updates`映射不为空时,系统才会调用`UpdateTextureFields`执行数据库更新操作。这种策略避免了不必要的数据库写入,提高了性能,并确保未提供的字段保持原值不变。
```mermaid
flowchart TD
Start([开始]) --> InitMap["初始化updates映射"]
InitMap --> CheckName{"名称提供且不为空?"}
CheckName --> |是| AddName["添加name到updates映射"]
CheckName --> |否| CheckDesc
AddName --> CheckDesc
CheckDesc --> {"描述提供且不为空?"}
CheckDesc --> |是| AddDesc["添加description到updates映射"]
CheckDesc --> |否| CheckPublic
AddDesc --> CheckPublic
CheckPublic --> {"公开状态提供?"}
CheckPublic --> |是| AddPublic["添加is_public到updates映射"]
CheckPublic --> |否| CheckUpdates
AddPublic --> CheckUpdates
CheckUpdates --> {"updates映射为空?"}
CheckUpdates --> |是| ReturnOriginal["返回原材质"]
CheckUpdates --> |否| UpdateDB["调用UpdateTextureFields<br/>更新数据库"]
UpdateDB --> ReturnUpdated["返回更新后的材质"]
ReturnOriginal --> End([结束])
ReturnUpdated --> End
```
**图示来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
## 错误处理场景
系统在材质上传与管理过程中实现了全面的错误处理机制。当用户达到上传上限时,`CheckTextureUploadLimit`函数返回"已达到最大上传数量限制"的错误信息。当用户尝试修改不属于自己的材质时,`UpdateTexture`函数返回"无权修改此材质"的错误信息。其他常见错误包括:用户不存在、材质已存在、无效的材质类型、文件名为空、不支持的文件格式等。所有错误都通过标准的错误响应格式返回给客户端,包含错误代码和描述信息,便于前端进行相应的错误处理和用户提示。
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
- [upload_service.go](file://internal/service/upload_service.go#L120-L127)
- [texture_service.go](file://internal/service/texture_service.go#L19-L21)
- [texture_service.go](file://internal/service/texture_service.go#L28-L30)
- [texture_service.go](file://internal/service/texture_service.go#L40-L41)

View File

@@ -1,327 +0,0 @@
# 材质上传流程
<cite>
**本文档引用文件**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
- [internal/model/texture.go](file://internal/model/texture.go)
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
- [pkg/config/config.go](file://pkg/config/config.go)
</cite>
## 目录
1. [流程概述](#流程概述)
2. [API接口说明](#api接口说明)
3. [生成预签名上传URL](#生成预签名上传url)
4. [创建材质记录](#创建材质记录)
5. [错误处理机制](#错误处理机制)
6. [流程时序图](#流程时序图)
## 流程概述
材质上传流程采用分步式设计包含两个核心API接口`GenerateTextureUploadURL``CreateTexture`。该流程遵循安全最佳实践通过预签名URL机制实现文件上传确保文件在上传到存储系统后才在数据库中创建相应记录。
整个流程分为两个阶段:
1. **准备阶段**:客户端调用`GenerateTextureUploadURL`接口服务器验证权限后返回预签名上传URL和表单数据
2. **完成阶段**:客户端使用返回的凭证上传文件后,调用`CreateTexture`接口创建材质元数据记录
这种设计确保了只有成功上传的文件才会在系统中创建记录,避免了数据库中出现孤立的记录。
## API接口说明
材质上传相关的API接口定义在路由配置中主要包含以下两个核心接口
```mermaid
flowchart TD
A[客户端] --> B[GenerateTextureUploadURL]
A --> C[CreateTexture]
B --> D[返回预签名URL和表单数据]
C --> E[创建材质记录]
```
**接口来源**
- [internal/handler/routes.go](file://internal/handler/routes.go#L53-L54)
## 生成预签名上传URL
`GenerateTextureUploadURL`接口负责生成临时的文件上传凭证,使客户端能够直接上传文件到对象存储系统。
### 接口实现
该接口的处理流程如下:
```mermaid
flowchart TD
Start([开始]) --> AuthCheck["验证用户身份"]
AuthCheck --> ValidateInput["验证请求参数"]
ValidateInput --> CheckFileName["验证文件名"]
CheckFileName --> CheckTextureType["验证材质类型"]
CheckTextureType --> GetStorageConfig["获取存储配置"]
GetStorageConfig --> GenerateObjectName["生成对象名称"]
GenerateObjectName --> GeneratePresignedURL["生成预签名POST URL"]
GeneratePresignedURL --> ReturnResult["返回PostURL和FormData"]
ReturnResult --> End([结束])
```
**代码来源**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L28-L83)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L117-L160)
### 请求参数
请求体包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| fileName | string | 上传的文件名 |
| textureType | string | 材质类型SKIN或CAPE |
### 响应格式
成功响应包含预签名上传所需的所有信息:
```json
{
"code": 200,
"message": "success",
"data": {
"postURL": "https://storage.example.com/textures",
"formData": {
"key": "user_1/skin/20231201120000_texture.png",
"policy": "base64-encoded-policy",
"signature": "request-signature",
"AWSAccessKeyId": "access-key-id"
},
"textureURL": "https://storage.example.com/textures/user_1/skin/20231201120000_texture.png",
"expiresIn": 900
}
}
```
### 实现细节
1. **文件名验证**:检查文件扩展名是否为`.png`确保只允许上传PNG格式的材质文件
2. **类型验证**:确认材质类型为`SKIN``CAPE`之一
3. **对象名称生成**:采用`user_{userId}/{textureType}/timestamp_{originalFileName}`的格式,确保文件路径的唯一性
4. **预签名URL生成**:调用存储模块的`GeneratePresignedPostURL`方法创建临时上传凭证
**存储实现来源**
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L120)
## 创建材质记录
`CreateTexture`接口在文件上传完成后创建材质的元数据记录,将文件与用户关联起来。
### 接口实现
该接口的处理流程包含多个验证步骤:
```mermaid
flowchart TD
Start([开始]) --> AuthCheck["验证用户身份"]
AuthCheck --> ValidateInput["验证请求参数"]
ValidateInput --> CheckLimit["检查上传数量限制"]
CheckLimit --> CheckHash["检查文件Hash是否重复"]
CheckHash --> ValidateUser["验证用户存在"]
ValidateUser --> ConvertType["转换材质类型"]
ConvertType --> CreateRecord["创建材质记录"]
CreateRecord --> ReturnResult["返回材质信息"]
ReturnResult --> End([结束])
style CheckLimit fill:#f9f,stroke:#333
style CheckHash fill:#f9f,stroke:#333
```
**代码来源**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L95-L172)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
### 请求参数
请求体包含材质的元数据信息:
| 字段 | 类型 | 说明 |
|------|------|------|
| name | string | 材质名称 |
| description | string | 描述信息 |
| type | string | 材质类型SKIN或CAPE |
| url | string | 文件访问URL |
| hash | string | 文件SHA-256哈希值 |
| size | int | 文件大小(字节) |
| isPublic | boolean | 是否公开 |
| isSlim | boolean | 是否为细身模型 |
### 响应格式
成功创建材质后返回材质的详细信息:
```json
{
"code": 200,
"message": "success",
"data": {
"id": 123,
"uploaderID": 1,
"name": "My Skin",
"description": "A custom skin",
"type": "SKIN",
"url": "https://storage.example.com/textures/user_1/skin/20231201120000_texture.png",
"hash": "sha256-hash-value",
"size": 10240,
"isPublic": true,
"downloadCount": 0,
"favoriteCount": 0,
"isSlim": false,
"status": 1,
"createdAt": "2023-12-01T12:00:00Z",
"updatedAt": "2023-12-01T12:00:00Z"
}
}
```
### 核心验证逻辑
#### 上传数量限制检查
系统通过`CheckTextureUploadLimit`函数检查用户是否达到上传上限:
```go
func CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) error {
count, err := repository.CountTexturesByUploaderID(uploaderID)
if err != nil {
return err
}
if count >= int64(maxTextures) {
return fmt.Errorf("已达到最大上传数量限制(%d)", maxTextures)
}
return nil
}
```
**代码来源**
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L223-L231)
#### 重复上传防止
通过文件的SHA-256哈希值防止重复上传相同内容的材质
```go
// 检查Hash是否已存在
existingTexture, err := repository.FindTextureByHash(hash)
if err != nil {
return nil, err
}
if existingTexture != nil {
return nil, errors.New("该材质已存在")
}
```
**代码来源**
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L30)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L29-L41)
## 错误处理机制
系统针对不同场景提供了详细的错误响应,帮助客户端正确处理各种异常情况。
### 错误码定义
| 错误码 | HTTP状态码 | 说明 |
|--------|------------|------|
| 400 | 400 | 请求参数错误 |
| 401 | 401 | 未授权访问 |
| 403 | 403 | 无权操作 |
| 404 | 404 | 资源不存在 |
### 常见错误场景
#### 上传数量达到上限
当用户上传的材质数量达到系统限制时,返回以下错误:
```json
{
"code": 400,
"message": "已达到最大上传数量限制(100)",
"data": null
}
```
#### 权限不足
未登录用户或非上传者尝试操作时返回:
```json
{
"code": 401,
"message": "Unauthorized",
"data": null
}
```
#### 文件Hash冲突
上传已存在的材质文件时返回:
```json
{
"code": 400,
"message": "该材质已存在",
"data": null
}
```
#### 无效的材质类型
指定不支持的材质类型时返回:
```json
{
"code": 400,
"message": "无效的材质类型: INVALID",
"data": null
}
```
## 流程时序图
以下是完整的材质上传流程时序图:
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "处理器"
participant Service as "服务层"
participant Storage as "存储模块"
participant DB as "数据库"
Client->>Handler : 调用GenerateTextureUploadURL
Handler->>Service : 验证参数并调用GenerateTextureUploadURL
Service->>Service : 验证文件名和类型
Service->>Storage : 获取存储桶名称
Storage-->>Service : 返回存储桶名称
Service->>Storage : 生成预签名POST URL
Storage-->>Service : 返回PostURL和FormData
Service-->>Handler : 返回结果
Handler-->>Client : 返回预签名上传信息
Client->>Storage : 使用PostURL上传文件
Storage-->>Client : 上传成功响应
Client->>Handler : 调用CreateTexture
Handler->>Service : 验证参数并调用CreateTexture
Service->>DB : 检查用户上传数量限制
DB-->>Service : 返回数量统计
Service->>DB : 检查文件Hash是否重复
DB-->>Service : 返回查询结果
Service->>DB : 创建材质记录
DB-->>Service : 创建成功
Service-->>Handler : 返回材质信息
Handler-->>Client : 返回创建结果
```
**时序图来源**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
- [internal/service/texture_service.go](file://internal/service/texture_service.go)

View File

@@ -1,460 +0,0 @@
# 材质管理操作
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [auth.go](file://internal/middleware/auth.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [manager.go](file://pkg/database/manager.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向后端开发者与运维人员,系统化梳理材质管理模块的增删改查与收藏能力,重点覆盖以下内容:
- UpdateTexture 接口的权限控制与字段级更新策略
- DeleteTexture 接口的软删除与权限校验
- ToggleFavorite 接口的收藏切换与 FavoriteCount 同步
- GetTexture、SearchTextures、GetUserTextures 的使用方式、分页与过滤规则
- 请求/响应结构、认证要求与常见错误码说明
## 项目结构
材质管理相关代码采用典型的分层架构:
- 路由层:注册 API 路由与鉴权中间件
- 处理层HTTP 控制器,负责参数解析、鉴权、调用服务层并返回统一响应
- 服务层:业务逻辑编排,包含权限校验、字段级更新、软删除、收藏切换等
- 仓储层:数据库访问封装,提供查询、更新、计数等方法
- 模型层:实体定义与数据库映射
- 类型与响应:请求/响应结构体与统一响应模型
```mermaid
graph TB
subgraph "路由层"
R["routes.go<br/>注册纹理相关路由"]
end
subgraph "中间件"
M["auth.go<br/>JWT鉴权中间件"]
end
subgraph "处理层"
H["texture_handler.go<br/>控制器:更新/删除/收藏/查询"]
end
subgraph "服务层"
S["texture_service.go<br/>业务逻辑:权限/字段更新/软删除/收藏切换"]
end
subgraph "仓储层"
REPO["texture_repository.go<br/>数据库访问:查询/更新/计数"]
end
subgraph "模型层"
MOD["texture.go<br/>实体Texture/UserTextureFavorite"]
end
subgraph "类型与响应"
T["common.go<br/>请求/响应结构体"]
RESP["response.go<br/>统一响应模型"]
end
DB["manager.go<br/>数据库初始化/迁移"]
R --> M --> H --> S --> REPO --> DB
H --> T
H --> RESP
S --> MOD
REPO --> MOD
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
## 核心组件
- 路由与鉴权
- 纹理路由组在 v1 下注册,其中部分接口需 JWT 鉴权;公开接口无需认证
- 鉴权中间件从 Authorization 头提取 Bearer Token 并校验,通过后将用户信息注入上下文
- 处理器
- 提供 UpdateTexture、DeleteTexture、ToggleFavorite、GetTexture、SearchTextures、GetUserTextures 等接口
- 参数绑定、分页默认值设置、统一响应封装
- 服务层
- UpdateTexture 字段级更新策略:仅当字段非空/非零时才更新
- DeleteTexture 软删除:将 Status 设为删除态,不影响数据完整性
- ToggleFavorite根据收藏状态切换同步更新 FavoriteCount
- 查询接口:分页参数校验与默认值处理
- 仓储层
- 提供按条件查询、分页、计数、字段更新、软删除、收藏相关 CRUD 等
- 模型层
- Texture包含上传者、名称、描述、类型、URL、哈希、大小、公开状态、下载/收藏计数、状态等
- UserTextureFavorite收藏关联表
- 类型与响应
- UpdateTextureRequest、CreateTextureRequest、TextureInfo 等结构体
- 统一响应模型与常用状态码
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
## 架构总览
下图展示“更新材质”接口的端到端调用链路,体现鉴权、参数校验、权限检查、字段级更新与返回结果的完整流程。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由(routes.go)"
participant MW as "鉴权中间件(auth.go)"
participant H as "控制器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant REPO as "仓储层(texture_repository.go)"
participant DB as "数据库"
C->>R : "PUT /api/v1/texture/ : id"
R->>MW : "进入AuthMiddleware()"
MW-->>R : "校验通过注入user_id"
R->>H : "转发到UpdateTexture处理器"
H->>H : "参数绑定/校验"
H->>S : "service.UpdateTexture(id, user_id, name, description, is_public?)"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "返回Texture或nil"
S->>S : "权限校验UploaderID==user_id"
S->>S : "构建字段更新map仅非空/非零字段"
S->>REPO : "UpdateTextureFields(id, updates)"
REPO->>DB : "执行UPDATE"
DB-->>REPO : "OK"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "返回更新后的Texture"
S-->>H : "返回Texture"
H-->>C : "200 成功响应"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L50-L60)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
## 详细组件分析
### UpdateTexture 接口:权限控制与字段级更新
- 权限控制
- 仅允许材质上传者修改;服务层通过比较 Texture 的 UploaderID 与请求用户ID进行校验
- 字段级更新策略
- 仅当请求中的 Name 非空、Description 非空、IsPublic 指针非空时,才将其加入更新集合
- 若无字段需要更新,则跳过数据库写入
- 最终重新查询并返回更新后的材质对象
- 认证要求
- 需携带 Bearer Token 的 Authorization 头
- 常见错误码
- 401 未授权(缺失/无效Token
- 403 权限不足(非上传者)
- 400 请求参数错误如ID非法
```mermaid
flowchart TD
Start(["进入UpdateTexture"]) --> Parse["解析请求参数与用户ID"]
Parse --> Load["加载材质记录"]
Load --> Exists{"是否存在且未删除?"}
Exists --> |否| Err404["返回404/不存在"]
Exists --> |是| Perm["校验权限UploaderID==user_id"]
Perm --> |否| Err403["返回403/权限不足"]
Perm --> |是| Build["构建字段更新map仅非空/非零字段"]
Build --> HasAny{"是否有字段需要更新?"}
HasAny --> |否| Reload["直接重新查询并返回"]
HasAny --> |是| Save["执行字段更新"]
Save --> Reload["重新查询并返回"]
Reload --> End(["结束"])
Err404 --> End
Err403 --> End
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
### DeleteTexture 接口:软删除与权限验证
- 软删除实现
- 仓储层通过将 Status 更新为删除态(-1实现软删除保留数据以满足审计与历史追踪
- 权限验证
- 仅允许材质上传者删除;服务层在删除前进行权限校验
- 认证要求
- 需携带 Bearer Token 的 Authorization 头
- 常见错误码
- 401 未授权
- 403 权限不足
- 400 请求参数错误如ID非法
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "控制器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant REPO as "仓储层(texture_repository.go)"
participant DB as "数据库"
C->>H : "DELETE /api/v1/texture/ : id"
H->>S : "service.DeleteTexture(id, user_id)"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "返回Texture或nil"
S->>S : "权限校验UploaderID==user_id"
S->>REPO : "DeleteTexture(id) -> 更新status=-1"
REPO->>DB : "执行UPDATE"
DB-->>REPO : "OK"
S-->>H : "返回nil成功"
H-->>C : "200 成功响应"
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
### ToggleFavorite 接口:收藏切换与计数同步
- 功能流程
- 检查材质是否存在且未删除
- 查询当前用户是否已收藏
- 已收藏:取消收藏并递减 FavoriteCount
- 未收藏:添加收藏并递增 FavoriteCount
- 返回新的收藏状态
- 认证要求
- 需携带 Bearer Token 的 Authorization 头
- 常见错误码
- 401 未授权
- 400 请求参数错误或业务异常
```mermaid
flowchart TD
Start(["进入ToggleFavorite"]) --> Load["加载材质记录"]
Load --> Exists{"是否存在且未删除?"}
Exists --> |否| Err["返回错误"]
Exists --> |是| Check["查询是否已收藏"]
Check --> Favorited{"已收藏?"}
Favorited --> |是| Unfav["删除收藏记录"] --> Dec["递减FavoriteCount"] --> RetFalse["返回false"]
Favorited --> |否| Fav["新增收藏记录"] --> Inc["递增FavoriteCount"] --> RetTrue["返回true"]
RetFalse --> End(["结束"])
RetTrue --> End
Err --> End
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
### 查询接口GetTexture、SearchTextures、GetUserTextures
- GetTexture
- 公开接口,无需认证
- 根据ID查询材质若不存在或已被软删除则返回404
- SearchTextures
- 公开接口,无需认证
- 支持关键词、类型、公开筛选;分页参数默认值与范围校验
- GetUserTextures
- 需 JWT 鉴权
- 仅返回当前用户上传且未删除的材质,支持分页
- 分页机制
- page 默认 1pageSize 默认 20最大 100
- 数据过滤规则
- SearchTexturesstatus=1可按 is_public 过滤;按 name/description 模糊匹配
- GetUserTexturesuploader_id=user_id 且 status!=-1
- GetTexturestatus!=deleted
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "控制器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant REPO as "仓储层(texture_repository.go)"
C->>H : "GET /api/v1/texture/ : id"
H->>S : "service.GetTextureByID(id)"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "Texture或nil"
S-->>H : "返回Texture或错误"
H-->>C : "200/404"
C->>H : "GET /api/v1/texture?page&page_size&type=SKIN&public_only=true"
H->>S : "service.SearchTextures(keyword,type,public_only,page,pageSize)"
S->>REPO : "SearchTextures(...)"
REPO-->>S : "[]Texture, total"
S-->>H : "返回列表与总数"
H-->>C : "200 分页响应"
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L174-L291)
- [texture_service.go](file://internal/service/texture_service.go#L66-L104)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L174-L291)
- [texture_service.go](file://internal/service/texture_service.go#L66-L104)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 依赖分析
- 组件耦合
- 路由层仅负责注册与中间件装配,低耦合
- 处理器依赖服务层,服务层依赖仓储层,仓储层依赖数据库
- 外部依赖
- JWT 鉴权中间件依赖认证服务
- 数据库初始化与自动迁移由数据库管理器负责
- 潜在循环依赖
- 代码组织清晰,未发现循环依赖迹象
```mermaid
graph LR
Routes["routes.go"] --> Auth["auth.go"]
Routes --> Handler["texture_handler.go"]
Handler --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> DBMgr["manager.go"]
Handler --> Types["common.go"]
Handler --> Resp["response.go"]
Service --> Model["texture.go"]
Repo --> Model
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
## 性能考虑
- 分页与索引
- 查询接口均使用分页与排序,仓储层对关键字段建立索引(如公开状态、下载/收藏计数、创建时间等),有助于提升检索性能
- 字段级更新
- UpdateTexture 仅更新提供字段,减少不必要的数据库写入
- 软删除
- 通过状态字段实现软删除,避免全量删除带来的性能与数据恢复成本
- 并发与事务
- 仓储层使用 GORM 执行单条 UPDATE/计数更新,建议在高并发场景下结合数据库层面的乐观锁或唯一约束保障一致性
[本节为通用指导,不涉及具体文件分析]
## 故障排查指南
- 401 未授权
- 检查 Authorization 头格式是否为 Bearer TokenToken 是否有效
- 403 权限不足
- 确认请求用户是否为材质上传者;检查 Update/Delete/ToggleFavorite 的权限校验逻辑
- 400 请求参数错误
- 检查 ID 是否为合法整数;请求体字段是否符合绑定规则
- 404 资源不存在
- 确认材质是否存在且未被软删除;查询接口会过滤 status=-1 的记录
- 分页异常
- page/page_size 默认值与范围校验page<1 设为 1pageSize 超过 100 或小于 1 设为 20
章节来源
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L66-L104)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 结论
本模块围绕最小权限+字段级更新+软删除+计数同步的设计原则提供了完善的材质管理能力通过鉴权中间件与服务层权限校验确保只有上传者可修改/删除材质UpdateTexture 的字段级更新策略降低写放大ToggleFavorite 在变更收藏状态的同时同步 FavoriteCount保证数据一致性查询接口提供灵活的过滤与分页能力兼顾可用性与性能
[本节为总结性内容不涉及具体文件分析]
## 附录
### 接口一览与认证要求
- GET /api/v1/texture/:id
- 认证
- 功能获取材质详情
- GET /api/v1/texture
- 认证
- 功能搜索材质关键词类型公开筛选
- PUT /api/v1/texture/:id
- 认证Bearer
- 功能更新材质仅上传者
- DELETE /api/v1/texture/:id
- 认证Bearer
- 功能软删除材质仅上传者
- POST /api/v1/texture/:id/favorite
- 认证Bearer
- 功能切换收藏状态
- GET /api/v1/texture/my
- 认证Bearer
- 功能获取当前用户上传的材质列表
- GET /api/v1/texture/favorites
- 认证Bearer
- 功能获取当前用户收藏的材质列表
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
### 请求/响应结构与字段说明
- UpdateTextureRequest
- 字段namedescriptionis_public指针
- 说明仅当字段非空/非零时参与更新
- CreateTextureRequest
- 字段namedescriptiontypeurlhashsizeis_publicis_slim
- TextureInfo
- 字段iduploader_idnamedescriptiontypeurlhashsizeis_publicdownload_countfavorite_countis_slimstatuscreated_atupdated_at
- 统一响应模型
- 成功code=200message=“操作成功”data=业务数据
- 错误code=400/401/403/404/500message=错误描述error=详细错误信息(开发环境)
章节来源
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)

View File

@@ -1,306 +0,0 @@
# 材质删除
<cite>
**本文引用的文件**
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
- [internal/model/texture.go](file://internal/model/texture.go)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
- [internal/handler/routes.go](file://internal/handler/routes.go)
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本专项文档聚焦“材质删除”API围绕删除操作的权限验证机制展开结合服务层与仓储层的实现说明删除流程、成功响应、错误场景以及删除对数据库记录的影响软删除与关联数据处理收藏关系。同时基于测试用例确认删除权限校验逻辑与行为一致性。
## 项目结构
与材质删除相关的代码分布在以下模块:
- 路由注册定义DELETE /api/v1/texture/:id接口
- 处理器:解析参数、鉴权、调用服务层并返回响应
- 服务层:执行业务规则(权限校验、存在性校验)、调用仓储层
- 仓储层:执行数据库操作(软删除)
- 数据模型:定义材质实体、收藏关系、下载日志等
```mermaid
graph TB
Routes["路由注册<br/>routes.go"] --> Handler["处理器<br/>texture_handler.go"]
Handler --> Service["服务层<br/>texture_service.go"]
Service --> Repo["仓储层<br/>texture_repository.go"]
Repo --> Model["数据模型<br/>texture.go"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
## 核心组件
- 路由与鉴权
- DELETE /api/v1/texture/:id 由处理器绑定,使用鉴权中间件,要求携带有效令牌。
- 处理器
- 解析路径参数材质ID从上下文提取当前用户ID调用服务层执行删除并按错误码返回相应HTTP状态与响应体。
- 服务层
- 校验材质存在性;校验删除权限(仅上传者可删);调用仓储层执行软删除。
- 仓储层
- 将材质记录的status字段置为-1实现软删除。
- 数据模型
- 材质实体包含UploaderID、Status等字段收藏关系通过user_texture_favorites表维护。
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
## 架构总览
下图展示从客户端到数据库的完整调用链路,包括鉴权、参数解析、权限校验、软删除与响应返回。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由(routes.go)"
participant H as "处理器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant P as "仓储层(texture_repository.go)"
participant D as "数据库"
C->>R : "DELETE /api/v1/texture/ : id"
R->>H : "进入处理器"
H->>H : "解析路径参数/校验JWT"
H->>S : "service.DeleteTexture(db, textureID, uploaderID)"
S->>P : "FindTextureByID(textureID)"
P-->>S : "返回材质或nil"
alt "材质不存在"
S-->>H : "返回错误:材质不存在"
H-->>C : "403 错误响应"
else "材质存在"
S->>S : "校验 uploaderID == 请求者ID"
alt "非上传者"
S-->>H : "返回错误:无权删除此材质"
H-->>C : "403 错误响应"
else "上传者本人"
S->>P : "DeleteTexture(textureID)"
P->>D : "UPDATE textures SET status=-1 WHERE id=..."
D-->>P : "OK"
P-->>S : "OK"
S-->>H : "OK"
H-->>C : "200 成功响应"
end
end
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
## 详细组件分析
### 删除API请求流程与权限验证
- 路由与鉴权
- DELETE /api/v1/texture/:id 由处理器绑定,使用鉴权中间件,要求携带有效令牌。
- 参数解析与鉴权
- 处理器从路径参数解析材质ID从上下文提取当前用户ID若缺失则返回未授权。
- 权限验证
- 服务层先查询材质是否存在且未被软删除;
- 再校验请求者ID与材质的UploaderID一致否则返回无权删除。
- 执行删除
- 通过仓储层将材质记录的status字段置为-1完成软删除。
- 响应
- 成功返回200与通用成功响应体错误返回403及错误信息。
```mermaid
flowchart TD
Start(["开始"]) --> Parse["解析路径参数<br/>获取材质ID"]
Parse --> Auth{"JWT鉴权通过"}
Auth --> |否| Resp401["返回401 未授权"]
Auth --> |是| Load["查询材质记录"]
Load --> Exists{"是否存在且未软删除?"}
Exists --> |否| Resp403a["返回403 材质不存在/已删除"]
Exists --> |是| Perm{"请求者ID==上传者ID"}
Perm --> |否| Resp403b["返回403 无权删除此材质"]
Perm --> |是| SoftDel["软删除设置status=-1"]
SoftDel --> Resp200["返回200 成功"]
Resp401 --> End(["结束"])
Resp403a --> End
Resp403b --> End
Resp200 --> End
```
图表来源
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
### 权限验证机制
- 上传者身份判定
- 服务层比较请求者的用户ID与材质记录中的UploaderID仅当两者相等时才允许删除。
- 存在性与状态校验
- 查询材质时会忽略status=-1的记录软删除因此若返回nil即视为“不存在”。
- 测试覆盖
- 单元测试包含“删除权限检查”的用例验证相同ID允许删除、不同ID拒绝删除的行为。
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L315-L345)
### 删除API的成功响应与错误情况
- 成功响应
- HTTP 200返回通用成功响应体无额外数据
- 常见错误
- 401 未授权缺少或无效的JWT令牌。
- 400 参数错误材质ID格式非法。
- 403 无权删除:请求者非材质上传者。
- 403 材质不存在:目标材质不存在或已被软删除。
- 处理器侧错误分支
- 参数解析失败返回400
- 服务层返回错误时统一转换为403无权删除/不存在)。
章节来源
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
### 数据库记录影响与关联数据处理
- 删除策略
- 采用软删除将textures表的status字段置为-1不物理移除记录。
- 影响范围
- 材质记录仍保留,便于审计与历史追踪;
- 查询接口如获取详情、搜索、我的材质均会忽略status=-1的记录。
- 关联数据
- 收藏关系删除操作不直接清理user_texture_favorites表中的收藏记录
- 若需清理收藏,应在业务层面另行设计或在仓储层扩展软删除时级联处理(当前实现未体现)。
- 下载日志
- 删除操作不涉及texture_download_logs表的清理。
章节来源
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
### 类图(代码级)
```mermaid
classDiagram
class Texture {
+int64 id
+int64 uploader_id
+string name
+string description
+TextureType type
+string url
+string hash
+int size
+bool is_public
+int download_count
+int favorite_count
+bool is_slim
+int16 status
+time created_at
+time updated_at
}
class UserTextureFavorite {
+int64 id
+int64 user_id
+int64 texture_id
+time created_at
}
class TextureDownloadLog {
+int64 id
+int64 texture_id
+*int64 user_id
+string ip_address
+string user_agent
+time created_at
}
Texture --> UserTextureFavorite : "收藏关系"
Texture --> TextureDownloadLog : "下载日志"
```
图表来源
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
- [internal/model/texture.go](file://internal/model/texture.go#L42-L57)
- [internal/model/texture.go](file://internal/model/texture.go#L60-L71)
## 依赖分析
- 组件耦合
- 处理器依赖服务层;服务层依赖仓储层;仓储层依赖数据库访问工具。
- 关键依赖链
- 路由 -> 处理器 -> 服务层 -> 仓储层 -> 数据库
- 可能的循环依赖
- 当前文件间未发现循环导入;各层职责清晰,符合分层架构。
```mermaid
graph LR
Routes["routes.go"] --> Handler["texture_handler.go"]
Handler --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> DB["数据库"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
## 性能考虑
- 查询与软删除
- 删除前的查询与软删除均为单记录操作,复杂度低。
- 索引与过滤
- 材质查询通常按UploaderID、状态、公开性等条件过滤建议确保相关列具备索引以提升查询效率。
- 批量与事务
- 当前删除为单条记录操作,无需事务;若未来扩展批量删除,需评估事务边界与回滚策略。
## 故障排查指南
- 401 未授权
- 检查请求头Authorization是否携带有效JWT确认中间件已正确注入user_id。
- 400 参数错误
- 检查路径参数id是否为合法整数。
- 403 无权删除
- 确认当前用户ID与材质记录的UploaderID一致核对服务层权限校验逻辑。
- 403 材质不存在
- 确认材质ID有效且未被软删除检查仓储层查询是否正确忽略status=-1。
- 日志定位
- 处理器与服务层均记录错误日志,可据此快速定位问题。
章节来源
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
## 结论
- 权限验证严格仅上传者可删除材质服务层明确校验请求者ID与UploaderID一致性。
- 删除策略为软删除通过status字段标记删除不破坏历史数据与关联完整性。
- 错误处理清晰处理器将服务层错误映射为403配合日志便于排障。
- 关联清理:当前实现未清理收藏关系,如需可在业务层补充或扩展仓储层软删除逻辑。

View File

@@ -1,243 +0,0 @@
# 材质搜索
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [common.go](file://internal/types/common.go)
- [texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向“材质搜索API”的使用与实现围绕关键词搜索、材质类型过滤、公开性筛选与分页功能进行深入解析。基于仓库中的 SearchTextures 查询流程解释关键词匹配名称与描述、类型过滤、公开状态筛选的实现细节并结合分页测试用例说明分页参数的处理规则page小于1时设为1pageSize超过100时设为20。同时提供请求示例、响应数据结构说明与错误处理机制并解释搜索结果中上传者信息的预加载机制Preload及其对性能的影响。
## 项目结构
材质搜索API由三层协作完成
- 路由层:注册 /api/v1/texture 的 GET 搜索接口
- 处理层:解析查询参数、调用服务层并返回分页响应
- 服务层:规范化分页参数、调用仓储层执行查询
- 仓储层:构建查询条件、统计总数、分页查询并预加载上传者信息
```mermaid
graph TB
Client["客户端"] --> Routes["路由: /api/v1/texture(GET)"]
Routes --> Handler["处理器: SearchTextures"]
Handler --> Service["服务: SearchTextures"]
Service --> Repo["仓储: SearchTextures"]
Repo --> DB["数据库"]
Handler --> Resp["分页响应: list,total,page,page_size,total_pages"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
章节来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
## 核心组件
- 路由注册:在 v1 组下将 GET /api/v1/texture 绑定到处理器 SearchTextures
- 处理器:读取 keyword、type、public_only、page、page_size 查询参数,调用服务层,转换为统一响应结构
- 服务层对分页参数进行边界校正page<1 设为1pageSize<1 >100 设为20再调用仓储层
- 仓储层:按状态=1 进行基础过滤;公开筛选、类型筛选、关键词模糊匹配;先 Count 再分页查询;使用 Preload 预加载上传者信息
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 架构总览
下面以序列图展示一次完整搜索请求的调用链路:
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由"
participant H as "处理器"
participant S as "服务层"
participant RE as "仓储层"
participant D as "数据库"
C->>R : GET /api/v1/texture?keyword=&type=&public_only=&page=&page_size=
R->>H : 调用 SearchTextures
H->>H : 解析查询参数<br/>keyword,type,public_only,page,page_size
H->>S : SearchTextures(db, keyword, type, public_only, page, page_size)
S->>S : 校正分页参数<br/>page<1→1pageSize<1或>100→20
S->>RE : SearchTextures(keyword, type, public_only, page, page_size)
RE->>D : 构建查询条件(status=1)<br/>公开筛选(is_public=?)<br/>类型筛选(type=?)
RE->>D : 关键词模糊匹配(name/description)<br/>Count统计总数
RE->>D : 分页查询(Offset/Limit)<br/>Preload上传者信息
D-->>RE : 结果集+总数
RE-->>S : 返回结果集+总数
S-->>H : 返回结果集+总数
H-->>C : 200 + 分页响应
```
图表来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 详细组件分析
### 请求参数与处理规则
- 查询参数
- keyword关键词支持名称与描述的模糊匹配
- type材质类型可选 SKIN/CAPE
- public_only布尔值仅返回公开材质
- page页码默认1
- page_size每页数量默认20
- 参数边界校正(服务层)
- page 小于1时设为1
- page_size 小于1时设为20大于100时也设为20
- 类型与公开筛选
- 类型为空字符串时不参与筛选
- public_only 为真时追加 is_public=true 条件
- 关键词匹配
- 同时对 name 和 description 使用 LIKE 模糊匹配
- 排序与分页
- 默认按 created_at 降序排序
- Offset=(page-1)*page_sizeLimit=page_size
- 预加载上传者信息
- 使用 Preload("Uploader") 预加载关联用户信息,便于直接返回上传者字段
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
### 数据模型与关联
- 材质模型包含上传者外键关联,仓储层通过 Preload 加载上传者信息
- 上传者信息在响应中以嵌套对象形式返回,便于前端直接显示
章节来源
- [texture.go](file://internal/model/texture.go#L16-L35)
### 响应数据结构
- 分页响应包含:
- list结果数组元素为材质信息
- total总条目数
- page当前页码
- page_size每页数量
- total_pages总页数由通用分页响应类型计算
章节来源
- [common.go](file://internal/types/common.go#L18-L25)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
### 错误处理机制
- 参数解析失败:返回 400 错误
- 服务内部错误:返回 500 错误
- 业务错误(如材质不存在等)在其他接口中体现,搜索接口主要返回 500 表示查询失败
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
### 分页测试用例要点
- page<1 1
- pageSize<1 20
- pageSize>100 → 20
- 以上规则在服务层统一应用,确保查询稳定性
章节来源
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
## 依赖关系分析
- 路由层依赖处理器层
- 处理器层依赖服务层
- 服务层依赖仓储层
- 仓储层依赖数据库层
- 响应结构依赖通用分页类型
```mermaid
graph LR
Routes["routes.go"] --> Handler["texture_handler.go"]
Handler --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> Model["texture.go"]
Handler --> Types["common.go"]
Service --> Types
```
图表来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [common.go](file://internal/types/common.go#L18-L25)
## 性能考量
- 预加载上传者信息Preload会增加单次查询的 JOIN 数量,导致额外的网络往返与内存占用。建议:
- 在高频搜索场景中评估是否需要返回上传者信息;若不需要,可在仓储层移除 Preload
- 对搜索结果进行缓存(如 Redis以减少重复 COUNT 与分页查询
- 为常用筛选维度建立合适索引(例如 idx_textures_public_type_status、idx_textures_download_count 等)
- 关键词模糊匹配使用 LIKE 百分号前缀可能导致索引失效,建议:
- 评估是否需要全文检索或倒排索引
- 对高并发场景考虑异步搜索或搜索引擎(如 Elasticsearch
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture.go](file://internal/model/texture.go#L16-L35)
## 故障排查指南
- 搜索无结果
- 检查 keyword 是否过短或包含特殊字符
- 确认 public_only 是否设置为 true 导致过滤掉私有材质
- 确认 type 是否正确传入SKIN/CAPE
- 分页异常
- page 小于1会被自动修正为1
- page_size 超过100会被修正为20
- 参数错误
- 确认查询参数类型与默认值是否符合预期
- 服务器错误
- 查看服务层日志,确认数据库连接与查询是否报错
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
## 结论
材质搜索API通过清晰的三层职责划分实现了关键词、类型与公开性三类筛选并以稳健的分页参数校正保障了查询稳定性。预加载上传者信息提升了前端展示效率但需关注其带来的性能成本。建议在生产环境中结合缓存与索引优化进一步提升搜索吞吐与延迟表现。
## 附录
### 请求示例
- 基础搜索(关键词)
- GET /api/v1/texture?keyword=steve
- 类型筛选
- GET /api/v1/texture?type=SKIN
- 公开性筛选
- GET /api/v1/texture?public_only=true
- 组合查询
- GET /api/v1/texture?keyword=cape&type=CAPE&public_only=true&page=1&page_size=20
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
### 响应示例
- 成功响应包含分页字段list、total、page、page_size、total_pages
- 每个材质项包含id、uploader_id、name、description、type、url、hash、size、is_public、download_count、favorite_count、is_slim、status、created_at、updated_at
章节来源
- [common.go](file://internal/types/common.go#L18-L25)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)

View File

@@ -1,411 +0,0 @@
# 材质收藏
<cite>
**本文引用的文件**
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [routes.go](file://internal/handler/routes.go)
- [texture.go](file://internal/model/texture.go)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql)
- [texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件围绕“材质收藏”能力进行系统化文档化,重点覆盖以下方面:
- 收藏/取消收藏的切换逻辑与幂等性设计
- 用户收藏列表的查询机制与分页实现
- API 的请求参数、响应格式与错误处理
- 基于测试用例的逻辑验证与正确性保障
- 数据模型与数据库结构支撑
## 项目结构
与“材质收藏”直接相关的代码分布在如下层次:
- 路由层:定义收藏相关接口路径
- 处理器层:解析请求、调用服务层、封装响应
- 服务层:业务逻辑(收藏切换、收藏列表查询)
- 仓储层:数据库访问(收藏状态判断、收藏增删、收藏计数增减、收藏列表查询)
- 模型层:数据结构(材质、收藏关系、下载日志)
- 数据库脚本:表结构与索引定义
```mermaid
graph TB
Routes["路由层<br/>routes.go"] --> Handler["处理器层<br/>texture_handler.go"]
Handler --> Service["服务层<br/>texture_service.go"]
Service --> Repo["仓储层<br/>texture_repository.go"]
Service --> Model["模型层<br/>texture.go"]
Repo --> DB["数据库脚本<br/>carrotskin_postgres.sql"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
## 核心组件
- 收藏切换接口POST /api/v1/texture/{id}/favorite
- 收藏列表接口GET /api/v1/texture/favorites
- 数据模型:材质、用户-材质收藏关系、下载日志
- 仓储方法:收藏状态判断、收藏增删、收藏计数增减、收藏列表查询
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture.go](file://internal/model/texture.go#L16-L57)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
## 架构总览
收藏能力的端到端流程如下:
- 客户端向收藏接口发起请求
- 路由层匹配到处理器
- 处理器解析参数、调用服务层
- 服务层根据收藏状态决定新增或删除收藏,并同步更新收藏计数
- 仓储层执行数据库操作
- 处理器封装响应返回客户端
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Routes as "路由层"
participant Handler as "处理器"
participant Service as "服务层"
participant Repo as "仓储层"
participant DB as "数据库"
Client->>Routes : "POST /api/v1/texture/{id}/favorite"
Routes->>Handler : "分发到 ToggleFavorite"
Handler->>Service : "ToggleTextureFavorite(userID, textureID)"
Service->>Repo : "IsTextureFavorited(userID, textureID)"
Repo-->>Service : "布尔结果"
alt 已收藏
Service->>Repo : "RemoveTextureFavorite(userID, textureID)"
Service->>Repo : "DecrementTextureFavoriteCount(textureID)"
Repo-->>Service : "OK"
else 未收藏
Service->>Repo : "AddTextureFavorite(userID, textureID)"
Service->>Repo : "IncrementTextureFavoriteCount(textureID)"
Repo-->>Service : "OK"
end
Service-->>Handler : "返回新的收藏状态"
Handler-->>Client : "200 {is_favorited : bool}"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L57-L57)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
## 详细组件分析
### 收藏切换 APIToggleTextureFavorite
- 接口路径POST /api/v1/texture/{id}/favorite
- 请求参数
- 路径参数id材质ID
- 认证Bearer TokenJWT
- 处理流程
- 校验材质存在性
- 查询当前用户是否已收藏
- 若已收藏:删除收藏记录并减少收藏计数
- 若未收藏:插入收藏记录并增加收藏计数
- 返回布尔值表示新的收藏状态
- 幂等性设计
- 同一用户对同一材质重复调用收藏/取消收藏,最终状态与最后一次操作一致
- 通过唯一约束避免重复收藏记录(见数据库脚本)
- 错误处理
- 材质不存在:返回错误
- 数据库异常:返回错误
- 未认证:返回 401
```mermaid
flowchart TD
Start(["进入 ToggleFavorite"]) --> Parse["解析路径参数 id"]
Parse --> CheckAuth["校验 JWT 有效性"]
CheckAuth --> CallSvc["调用服务层 ToggleTextureFavorite"]
CallSvc --> Exists{"材质存在?"}
Exists --> |否| Err["返回错误:材质不存在"]
Exists --> |是| Favorited{"是否已收藏?"}
Favorited --> |是| Unfav["删除收藏记录"]
Favorited --> |否| Fav["新增收藏记录"]
Unfav --> Dec["收藏计数 -1"]
Fav --> Inc["收藏计数 +1"]
Dec --> Ret["返回 false"]
Inc --> Ret2["返回 true"]
Err --> End(["结束"])
Ret --> End
Ret2 --> End
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
### 用户收藏列表 APIGetUserTextureFavorites
- 接口路径GET /api/v1/texture/favorites
- 请求参数
- page页码默认 1最小 1
- page_size每页数量默认 20最小 1最大 100
- 认证Bearer TokenJWT
- 处理流程
- 校验分页参数边界
- 通过子查询获取当前用户收藏的材质ID集合
- 基于材质状态过滤(仅返回正常状态)
- 分页查询并返回总数
- 响应格式
- 包含分页信息与材质列表(每项包含基础元信息)
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Routes as "路由层"
participant Handler as "处理器"
participant Service as "服务层"
participant Repo as "仓储层"
participant DB as "数据库"
Client->>Routes : "GET /api/v1/texture/favorites?page&page_size"
Routes->>Handler : "分发到 GetUserFavorites"
Handler->>Service : "GetUserTextureFavorites(userID, page, pageSize)"
Service->>Repo : "子查询获取收藏的 texture_id"
Repo-->>Service : "ID 列表"
Service->>Repo : "按状态过滤并分页查询材质"
Repo-->>Service : "材质列表 + 总数"
Service-->>Handler : "返回结果"
Handler-->>Client : "200 {data, total, page, page_size}"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L59-L59)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### 数据模型与数据库结构
- 材质模型textures
- 字段uploader_id、name、description、type、url、hash、size、is_public、download_count、favorite_count、is_slim、status、created_at、updated_at
- 索引:按 uploader_id、公开/类型/状态组合索引、收藏数降序索引
- 用户-材质收藏关系user_texture_favorites
- 字段user_id、texture_id、created_at
- 唯一键:(user_id, texture_id),防止重复收藏
- 索引user_id、texture_id、created_at
- 下载日志texture_download_logs
- 字段texture_id、user_id、ip_address、user_agent、created_at
- 用于统计与风控
```mermaid
erDiagram
USER {
bigint id PK
varchar username UK
varchar email UK
}
TEXTURES {
bigint id PK
bigint uploader_id FK
varchar name
text description
enum type
varchar url
varchar hash UK
integer size
boolean is_public
integer download_count
integer favorite_count
boolean is_slim
smallint status
timestamp created_at
timestamp updated_at
}
USER_TEXTURE_FAVORITES {
bigint id PK
bigint user_id FK
bigint texture_id FK
timestamp created_at
}
TEXTURE_DOWNLOAD_LOGS {
bigint id PK
bigint texture_id FK
bigint user_id FK
inet ip_address
text user_agent
timestamp created_at
}
USER ||--o{ TEXTURES : "上传"
USER ||--o{ USER_TEXTURE_FAVORITES : "收藏"
TEXTURES ||--o{ USER_TEXTURE_FAVORITES : "被收藏"
USER ||--o{ TEXTURE_DOWNLOAD_LOGS : "下载"
TEXTURES ||--o{ TEXTURE_DOWNLOAD_LOGS : "被下载"
```
图表来源
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L272-L292)
章节来源
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L272-L292)
### 幂等性与重复收藏防护
- 幂等性
- 对同一用户/材质重复调用收藏/取消收藏,最终状态与最后一次操作一致
- 重复收藏防护
- user_texture_favorites 表的唯一键 (user_id, texture_id) 防止重复插入
- 服务层通过“是否已收藏”的查询结果决定新增或删除,避免多余写入
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L172-L187)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
### 分页查询示例(收藏列表)
- 请求
- 方法GET
- 路径:/api/v1/texture/favorites
- 查询参数:
- page页码默认 1最小 1
- page_size每页数量默认 20最小 1最大 100
- 响应
- data材质数组每项包含基础元信息
- total总数
- page、page_size当前页与每页条数
- 错误处理
- 未认证401
- 服务器内部错误500
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### 测试用例验证要点
- 收藏切换逻辑
- 已收藏 -> 取消收藏:返回 false
- 未收藏 -> 添加收藏:返回 true
- 收藏列表分页
- page 小于 1自动修正为 1
- page_size 超过 100自动修正为 20
- 其他相关测试
- 材质类型验证、默认值、状态验证、分页边界等
章节来源
- [texture_service_test.go](file://internal/service/texture_service_test.go#L347-L374)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)
## 依赖分析
- 路由到处理器
- /api/v1/texture/{id}/favorite -> ToggleFavorite
- /api/v1/texture/favorites -> GetUserFavorites
- 处理器到服务层
- ToggleFavorite -> ToggleTextureFavorite
- GetUserFavorites -> GetUserTextureFavorites
- 服务层到仓储层
- ToggleTextureFavorite -> IsTextureFavorited、AddTextureFavorite、RemoveTextureFavorite、IncrementTextureFavoriteCount、DecrementTextureFavoriteCount
- GetUserTextureFavorites -> GetUserTextureFavorites子查询 + 分页)
- 仓储层到数据库
- 使用 GORM 执行查询与更新,依赖唯一键约束保证幂等
```mermaid
graph LR
R["routes.go"] --> H["texture_handler.go"]
H --> S["texture_service.go"]
S --> RE["texture_repository.go"]
RE --> D["carrotskin_postgres.sql"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
## 性能考虑
- 索引优化
- textures 表的 favorite_count 降序索引有利于排序与统计
- user_texture_favorites 的 user_id、texture_id 索引提升收藏查询与去重效率
- 写入优化
- 收藏计数采用原子更新(+1/-1避免额外查询
- 分页限制
- 服务层对 page_size 设定上限,防止大页导致的数据库压力
章节来源
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L68)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L99-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L139-L151)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
## 故障排查指南
- 常见错误
- 400无效的材质ID、请求参数错误
- 401未认证
- 404材质不存在
- 500服务器内部错误
- 排查步骤
- 确认 JWT 是否正确传递
- 校验路径参数 id 是否为有效整数
- 检查数据库连接与迁移是否完成
- 查看处理器日志定位具体错误点
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
## 结论
- 收藏功能以“取反操作”为核心,通过唯一键约束与服务层条件判断实现幂等
- 收藏列表查询采用子查询 + 分页,兼顾准确性与性能
- 测试用例覆盖了关键分支,确保逻辑正确性
- 数据库层面的索引与约束为高并发场景提供了基础保障
## 附录
- API 列表
- POST /api/v1/texture/{id}/favorite切换收藏状态
- GET /api/v1/texture/favorites获取用户收藏列表分页
- 关键实现位置
- 收藏切换:[texture_service.go](file://internal/service/texture_service.go#L189-L237)
- 收藏列表:[texture_service.go](file://internal/service/texture_service.go#L227-L237)、[texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
- 路由绑定:[routes.go](file://internal/handler/routes.go#L57-L59)
- 处理器实现:[texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)、[texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- 数据模型与表结构:[texture.go](file://internal/model/texture.go#L16-L57)、[carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)

View File

@@ -1,398 +0,0 @@
# 创建与列表
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [profile.go](file://internal/model/profile.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [texture.go](file://internal/model/texture.go)
- [profile_handler_test.go](file://internal/handler/profile_handler_test.go)
- [common_test.go](file://internal/types/common_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者与集成方,系统性梳理“创建与列表”相关接口的实现与使用规范,重点覆盖:
- 通过 POST /api/v1/profile/ 创建新档案,请求体中必须包含 1-16 字符的角色名可选皮肤ID与披风ID。
- 系统在创建时自动将该档案设为用户活跃档案,并将该用户其他档案置为非活跃。
- 通过 GET /api/v1/profile/ 获取当前用户所有档案列表响应包含档案UUID、名称、活跃状态、关联材质等信息。
- 结合 profile_service.go 中的 CheckProfileLimit 逻辑说明用户档案数量上限默认5个的控制机制。
## 项目结构
围绕档案模块的路由、处理器、服务层、仓储层与模型如下所示:
```mermaid
graph TB
subgraph "路由层"
R["routes.go<br/>注册 /api/v1/profile/* 路由"]
end
subgraph "处理器层"
H["profile_handler.go<br/>CreateProfile / GetProfiles / SetActiveProfile 等"]
end
subgraph "服务层"
S["profile_service.go<br/>CreateProfile / GetUserProfiles / CheckProfileLimit 等"]
end
subgraph "仓储层"
RP["profile_repository.go<br/>CreateProfile / FindProfilesByUserID / SetActiveProfile 等"]
end
subgraph "模型与类型"
M["profile.go<br/>Profile / ProfileResponse / ProfileTexturesData 等"]
T["texture.go<br/>Texture 类型"]
C["common.go<br/>CreateProfileRequest / ProfileInfo / UpdateProfileRequest 等"]
RESP["response.go<br/>统一响应结构"]
end
R --> H
H --> S
S --> RP
RP --> M
S --> M
H --> RESP
H --> C
M --> T
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
- [profile.go](file://internal/model/profile.go#L7-L57)
- [texture.go](file://internal/model/texture.go#L16-L31)
- [common.go](file://internal/types/common.go#L81-L166)
- [response.go](file://internal/model/response.go#L1-L86)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
## 核心组件
- 路由注册:在路由层为档案模块注册了认证中间件保护的 POST / GET / PUT / DELETE / POST activate 等路径。
- 处理器:负责解析请求、鉴权、调用服务层并返回统一响应。
- 服务层:封装业务规则,如创建档案时的用户存在性校验、角色名唯一性校验、活跃状态设置、档案数量上限检查等。
- 仓储层:封装数据库访问,如创建档案、查询用户档案列表、批量设置活跃状态等。
- 模型与类型:定义档案实体、响应结构、请求体结构以及材质类型。
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
- [profile.go](file://internal/model/profile.go#L7-L57)
- [common.go](file://internal/types/common.go#L81-L166)
- [response.go](file://internal/model/response.go#L1-L86)
## 架构总览
下图展示从客户端到数据库的调用链路与关键步骤。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Router as "路由层(routes.go)"
participant Handler as "处理器(profile_handler.go)"
participant Service as "服务层(profile_service.go)"
participant Repo as "仓储层(profile_repository.go)"
participant DB as "数据库"
Client->>Router : "POST /api/v1/profile"
Router->>Handler : "CreateProfile"
Handler->>Handler : "解析请求体(CreateProfileRequest)"
Handler->>Handler : "读取用户ID(鉴权)"
Handler->>Service : "CheckProfileLimit(userID, 5)"
Service->>Repo : "CountProfilesByUserID(userID)"
Repo->>DB : "统计数量"
DB-->>Repo : "count"
Repo-->>Service : "返回count"
Service-->>Handler : "通过/错误"
alt "未达上限"
Handler->>Service : "CreateProfile(userID, name)"
Service->>Repo : "FindUserByID(userID)"
Repo->>DB : "查询用户"
DB-->>Repo : "用户"
Repo-->>Service : "返回用户"
Service->>Repo : "FindProfileByName(name)"
Repo->>DB : "查询角色名"
DB-->>Repo : "结果"
Service->>Repo : "CreateProfile(Profile)"
Repo->>DB : "插入档案"
Service->>Repo : "SetActiveProfile(uuid, userID)"
Repo->>DB : "事务 : 将其他档案置为非活跃<br/>并将当前档案置为活跃"
DB-->>Repo : "提交"
Repo-->>Service : "返回Profile"
Service-->>Handler : "返回Profile"
Handler-->>Client : "200 成功(统一响应)"
else "已达上限"
Handler-->>Client : "400 参数错误(已达上限)"
end
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
- [profile_service.go](file://internal/service/profile_service.go#L17-L69)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
## 详细组件分析
### POST /api/v1/profile/ 创建档案
- 功能概述创建新的Minecraft角色档案系统自动生成UUID并默认设为活跃状态同时将该用户其他档案置为非活跃。
- 请求体
- 必填name字符串1-16字符
- 可选skin_id整数材质ID、cape_id整数材质ID
- 响应
- 成功返回统一响应结构data为 ProfileInfo 对象,包含 uuid、user_id、name、skin_id、cape_id、is_active、last_used_at、created_at、updated_at。
- 失败:根据错误类型返回 400参数错误/已达上限、401未授权、500服务器错误
- 关键业务逻辑
- 用户存在性与状态校验
- 角色名唯一性校验
- 档案数量上限检查默认5个
- 创建成功后自动设置活跃状态,并将其他档案置为非活跃
- 请求示例
- 方法POST
- URL/api/v1/profile
- 请求头Authorization: Bearer <token>
- 请求体:
- name: "PlayerName"
- skin_id: 123可选
- cape_id: 456可选
- 响应示例
- 成功:
- code: 200
- message: "操作成功"
- data: {
uuid: "550e8400-e29b-41d4-a716-446655440000"
user_id: 1
name: "PlayerName"
skin_id: 123
cape_id: 456
is_active: true
last_used_at: "2025-10-01T12:00:00Z"
created_at: "2025-10-01T10:00:00Z"
updated_at: "2025-10-01T10:00:00Z"
}
- 达到上限:
- code: 400
- message: "已达到档案数量上限5个"
```mermaid
flowchart TD
Start(["进入 CreateProfile"]) --> Bind["绑定请求体(CreateProfileRequest)"]
Bind --> CheckAuth{"鉴权通过?"}
CheckAuth --> |否| Resp401["返回 401 未授权"]
CheckAuth --> |是| Limit["CheckProfileLimit(userID, 5)"]
Limit --> Over{"超过上限?"}
Over --> |是| Resp400["返回 400 已达上限"]
Over --> |否| Create["CreateProfile(userID, name)"]
Create --> Exists{"用户存在且状态正常?"}
Exists --> |否| Resp500["返回 500 用户异常"]
Exists --> |是| Unique{"角色名唯一?"}
Unique --> |否| Resp400["返回 400 角色名冲突"]
Unique --> |是| Insert["插入档案记录"]
Insert --> SetActive["SetActiveProfile(uuid, userID)<br/>将其他档案置为非活跃,当前置为活跃"]
SetActive --> Done(["返回 200 成功"])
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
- [profile_service.go](file://internal/service/profile_service.go#L17-L69)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
- [profile_service.go](file://internal/service/profile_service.go#L17-L69)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
- [common.go](file://internal/types/common.go#L81-L90)
- [response.go](file://internal/model/response.go#L1-L86)
### GET /api/v1/profile/ 获取档案列表
- 功能概述返回当前用户的所有档案列表包含每个档案的UUID、名称、活跃状态、关联材质等。
- 请求
- 方法GET
- URL/api/v1/profile
- 请求头Authorization: Bearer <token>
- 响应
- 成功返回统一响应结构data为 ProfileInfo 数组。
- 失败401未授权、500服务器错误
- 关键逻辑
- 服务层查询用户所有档案并预加载关联材质Skin/Cape
- 处理器将模型转换为 ProfileInfo 并返回
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Router as "路由层(routes.go)"
participant Handler as "处理器(profile_handler.go)"
participant Service as "服务层(profile_service.go)"
participant Repo as "仓储层(profile_repository.go)"
participant DB as "数据库"
Client->>Router : "GET /api/v1/profile"
Router->>Handler : "GetProfiles"
Handler->>Handler : "读取用户ID(鉴权)"
Handler->>Service : "GetUserProfiles(userID)"
Service->>Repo : "FindProfilesByUserID(userID)"
Repo->>DB : "查询档案列表并预加载 Skin/Cape"
DB-->>Repo : "返回 profiles"
Repo-->>Service : "返回 profiles"
Service-->>Handler : "返回 profiles"
Handler->>Handler : "转换为 ProfileInfo 列表"
Handler-->>Client : "200 成功(统一响应)"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
- [profile_service.go](file://internal/service/profile_service.go#L83-L90)
- [profile_repository.go](file://internal/repository/profile_repository.go#L44-L57)
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
- [profile_service.go](file://internal/service/profile_service.go#L83-L90)
- [profile_repository.go](file://internal/repository/profile_repository.go#L44-L57)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [texture.go](file://internal/model/texture.go#L16-L31)
- [response.go](file://internal/model/response.go#L1-L86)
### 档案数量上限与活跃状态控制
- 档案数量上限
- 默认上限为5个来源于系统配置与处理器中的硬编码值。
- 服务层提供 CheckProfileLimit(userID, maxProfiles) 进行检查。
- 活跃状态控制
- 创建新档案时,服务层会将该档案设为活跃,并通过仓储层的事务将该用户其他档案置为非活跃。
- 提供 SetActiveProfile 接口用于手动切换活跃档案。
```mermaid
flowchart TD
A["创建/切换活跃"] --> B["CheckProfileLimit(userID, 5)"]
B --> C{"未超限?"}
C --> |是| D["CreateProfile 或 SetActiveProfile"]
D --> E["SetActiveProfile(uuid, userID) 事务"]
E --> F["将其他档案 is_active=false"]
E --> G["将当前档案 is_active=true"]
C --> |否| H["返回 400 达到上限"]
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L52-L63)
- [profile_service.go](file://internal/service/profile_service.go#L190-L202)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L52-L63)
- [profile_service.go](file://internal/service/profile_service.go#L190-L202)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
## 依赖分析
- 路由层依赖处理器层,处理器层依赖服务层,服务层依赖仓储层,仓储层依赖数据库与模型。
- 处理器层与服务层均依赖统一响应结构与请求/响应类型定义。
- 档案模型与材质模型存在外键关联查询时进行预加载以减少N+1问题。
```mermaid
graph LR
Routes["routes.go"] --> Handler["profile_handler.go"]
Handler --> Service["profile_service.go"]
Service --> Repo["profile_repository.go"]
Repo --> Model["profile.go"]
Model --> Texture["texture.go"]
Handler --> Types["common.go"]
Handler --> Resp["response.go"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
- [profile.go](file://internal/model/profile.go#L7-L57)
- [texture.go](file://internal/model/texture.go#L16-L31)
- [common.go](file://internal/types/common.go#L81-L166)
- [response.go](file://internal/model/response.go#L1-L86)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
- [profile.go](file://internal/model/profile.go#L7-L57)
- [texture.go](file://internal/model/texture.go#L16-L31)
- [common.go](file://internal/types/common.go#L81-L166)
- [response.go](file://internal/model/response.go#L1-L86)
## 性能考虑
- 预加载关联材质:仓储层在查询用户档案列表时预加载 Skin 与 Cape避免多次查询。
- 事务一致性:设置活跃状态采用数据库事务,确保原子性与一致性。
- 唯一性约束:角色名在模型层定义唯一索引,查询时可快速判定冲突。
- 响应结构:统一响应结构便于前端处理与日志记录。
[本节为通用建议,不涉及具体文件分析]
## 故障排查指南
- 400 参数错误
- 角色名为空或长度不在1-16范围内
- 已达到档案数量上限默认5个
- 401 未授权
- 缺少或无效的认证令牌
- 403 权限不足
- 操作他人档案(如更新/删除/设置活跃)
- 404 资源不存在
- 档案UUID不存在
- 500 服务器错误
- 数据库查询失败、事务提交失败、用户状态异常等
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L280)
- [profile_handler.go](file://internal/handler/profile_handler.go#L282-L339)
- [profile_handler.go](file://internal/handler/profile_handler.go#L341-L399)
- [profile_handler_test.go](file://internal/handler/profile_handler_test.go#L40-L73)
- [common_test.go](file://internal/types/common_test.go#L350-L383)
## 结论
- 创建档案接口严格遵循请求体校验与业务规则,确保角色名唯一与数量上限控制。
- 活跃状态切换通过事务保障一致性,避免并发场景下的状态不一致。
- 档案列表接口提供完整档案信息与关联材质,满足前端展示需求。
- 建议在生产环境中将上限值从硬编码迁移到系统配置中心,以便动态调整。
[本节为总结性内容,不涉及具体文件分析]
## 附录
### API 定义与数据结构
- POST /api/v1/profile
- 请求体CreateProfileRequest
- name: string (必填1-16字符)
- skin_id: int64 (可选)
- cape_id: int64 (可选)
- 响应体统一响应结构data 为 ProfileInfo
- uuid: string
- user_id: int64
- name: string
- skin_id: int64 (可选)
- cape_id: int64 (可选)
- is_active: bool
- last_used_at: datetime (可选)
- created_at: datetime
- updated_at: datetime
- GET /api/v1/profile
- 响应体统一响应结构data 为 ProfileInfo 数组
章节来源
- [common.go](file://internal/types/common.go#L81-L90)
- [common.go](file://internal/types/common.go#L154-L166)
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
- [response.go](file://internal/model/response.go#L1-L86)

View File

@@ -1,354 +0,0 @@
# 更新与删除
<cite>
**本文档引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [auth.go](file://internal/middleware/auth.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [profile.go](file://internal/model/profile.go)
- [common.go](file://internal/types/common.go)
- [jwt.go](file://pkg/auth/jwt.go)
- [manager.go](file://pkg/database/manager.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本章节面向需要对接“档案更新与删除”接口的开发者,重点说明以下内容:
- PUT /api/v1/profile/:uuid 的更新流程:支持修改档案名称、更换关联的皮肤或披风材质;强调“只能修改自己名下的档案”的权限校验。
- DELETE /api/v1/profile/:uuid 的删除流程:删除前进行权限检查与档案存在性验证。
- 请求体格式与错误处理策略如403无权操作、404资源不存在
- 结合 service 层代码解释更新操作的事务性保证与数据一致性维护机制。
## 项目结构
围绕档案更新与删除功能,涉及如下关键模块:
- 路由层:在路由中注册了 /api/v1/profile/{uuid} 的 PUT 与 DELETE 接口,并统一使用鉴权中间件。
- 中间件层JWT 鉴权中间件负责解析 Authorization 头并校验令牌有效性,将用户标识注入上下文。
- 处理器层profile_handler 负责接收请求、绑定参数、调用 service 并输出响应。
- 服务层profile_service 执行业务逻辑,包含权限校验、唯一性检查、更新与删除等。
- 仓储层profile_repository 封装数据库访问,提供查询、更新、删除与事务性操作。
- 模型与类型profile 模型定义档案字段及关联关系types 定义请求与响应结构。
- 数据库与鉴权GORM 管理数据库连接JWT 用于用户身份验证。
```mermaid
graph TB
Client["客户端"] --> Routes["路由: /api/v1/profile/{uuid}"]
Routes --> AuthMW["鉴权中间件: JWT"]
AuthMW --> Handler["处理器: profile_handler"]
Handler --> Service["服务: profile_service"]
Service --> Repo["仓储: profile_repository"]
Repo --> DB["数据库: GORM"]
Handler --> Model["模型: Profile"]
Handler --> Types["类型: UpdateProfileRequest"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
- [profile_service.go](file://internal/service/profile_service.go#L92-L159)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L77)
- [profile.go](file://internal/model/profile.go#L7-L29)
- [common.go](file://internal/types/common.go#L201-L206)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
## 核心组件
- 路由注册:在路由组 /api/v1/profile 下注册了 GET/:uuid、POST/、GET/、PUT/:uuid、DELETE/:uuid、POST/:uuid/activate 等接口,其中 PUT 与 DELETE 对应本节主题。
- 鉴权中间件:要求 Authorization 头为 Bearer 令牌,校验通过后将 user_id 等信息写入上下文。
- 处理器UpdateProfile 与 DeleteProfile 分别调用 service 层执行业务逻辑,并根据 service 返回的错误映射为合适的 HTTP 状态码。
- 服务层UpdateProfile 与 DeleteProfile 在执行前均进行“档案存在性验证”和“权限校验”,并在必要时使用数据库事务保证一致性。
- 仓储层FindProfileByUUID、UpdateProfile、DeleteProfile 提供基础 CRUDSetActiveProfile 使用事务确保“同一用户仅有一个活跃档案”。
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
- [profile_service.go](file://internal/service/profile_service.go#L92-L159)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
## 架构总览
下图展示了从客户端到数据库的调用链路,以及权限校验与事务控制的关键节点。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由"
participant M as "鉴权中间件"
participant H as "处理器 : UpdateProfile/DeleteProfile"
participant S as "服务 : profile_service"
participant P as "仓储 : profile_repository"
participant D as "数据库 : GORM"
C->>R : "PUT /api/v1/profile/{uuid}"
R->>M : "进入鉴权中间件"
M-->>R : "校验通过,注入 user_id"
R->>H : "转发请求"
H->>S : "调用 UpdateProfile/ DeleteProfile"
S->>P : "FindProfileByUUID"
P->>D : "查询档案"
D-->>P : "返回档案或记录不存在"
P-->>S : "返回结果"
alt "更新场景"
S->>S : "权限校验 : profile.UserID == user_id"
S->>P : "可选 : 名称唯一性检查"
S->>P : "UpdateProfile(更新字段)"
P->>D : "保存更新"
D-->>P : "成功"
P-->>S : "返回更新后的档案"
else "删除场景"
S->>S : "权限校验 : profile.UserID == user_id"
S->>P : "DeleteProfile"
P->>D : "删除记录"
D-->>P : "成功"
end
S-->>H : "返回结果或错误"
H-->>C : "HTTP 响应(200/403/404/500)"
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
- [profile_service.go](file://internal/service/profile_service.go#L92-L159)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L77)
## 详细组件分析
### PUT /api/v1/profile/:uuid 更新档案
- 功能概述
- 支持修改档案名称(可选)、更换关联的皮肤或披风材质(可选)。
- 严格权限控制:仅允许修改“自己名下的档案”。
- 请求路径与方法
- 方法: PUT
- 路径: /api/v1/profile/:uuid
- 鉴权: 需要 Bearer 令牌
- 请求体结构
- 字段:
- name: 字符串,长度范围 1-16可选
- skin_id: 整数指向材质记录的ID可选
- cape_id: 整数指向材质记录的ID可选
- 示例: 仅更新名称
- 示例: 仅更换皮肤
- 示例: 同时更换皮肤与披风
- 成功响应
- 返回更新后的档案信息(包含 uuid、user_id、name、skin_id、cape_id、is_active、last_used_at、created_at、updated_at
- 错误处理
- 400: 请求参数错误(如字段校验失败)
- 401: 未授权(缺少或无效的 Authorization 头)
- 403: 无权操作(目标档案不属于当前用户)
- 404: 资源不存在档案UUID不存在
- 500: 服务器内部错误(数据库异常、唯一性冲突等)
- 权限与存在性校验
- 处理器从上下文取出 user_id若缺失则直接返回 401。
- 服务层先查询档案,若不存在返回 404随后校验 profile.UserID 是否等于 user_id否则返回 403。
- 名称唯一性与字段更新
- 当 name 发生变化时,服务层会检查同名是否已存在,若存在则返回 400。
- skin_id 与 cape_id 为可选字段,仅当传入非空值时才更新对应字段。
- 事务性与一致性
- 更新操作本身通过单条 save/update 完成,不涉及跨表事务。
- 若未来扩展为多步更新(例如同时更新多个关联字段),建议在服务层使用 GORM 事务包裹,确保原子性与一致性。
```mermaid
flowchart TD
Start(["进入 UpdateProfile"]) --> Parse["解析请求体<br/>name/skin_id/cape_id"]
Parse --> GetCtx["从上下文获取 user_id"]
GetCtx --> HasUser{"user_id 存在?"}
HasUser --> |否| Resp401["返回 401 未授权"]
HasUser --> |是| Load["查询档案 FindProfileByUUID"]
Load --> Found{"档案存在?"}
Found --> |否| Resp404["返回 404 资源不存在"]
Found --> |是| Perm{"档案归属校验<br/>profile.UserID == user_id"}
Perm --> |否| Resp403["返回 403 无权操作"]
Perm --> |是| NameChk{"是否更新 name"}
NameChk --> |是| Dup{"检查同名是否存在"}
Dup --> |是| Resp400["返回 400 参数错误"]
Dup --> |否| Apply["应用字段更新<br/>name/skin_id/cape_id"]
NameChk --> |否| Apply
Apply --> Save["保存更新 UpdateProfile"]
Save --> Reload["重新加载档案 FindProfileByUUID"]
Reload --> Resp200["返回 200 成功"]
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L280)
- [profile_service.go](file://internal/service/profile_service.go#L92-L135)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L71)
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L280)
- [profile_service.go](file://internal/service/profile_service.go#L92-L135)
- [common.go](file://internal/types/common.go#L201-L206)
### DELETE /api/v1/profile/:uuid 删除档案
- 功能概述
- 删除指定 UUID 的档案。
- 删除前进行权限检查与档案存在性验证。
- 请求路径与方法
- 方法: DELETE
- 路径: /api/v1/profile/:uuid
- 鉴权: 需要 Bearer 令牌
- 成功响应
- 返回成功消息message: "删除成功")。
- 错误处理
- 401: 未授权(缺少或无效的 Authorization 头)
- 403: 无权操作(目标档案不属于当前用户)
- 404: 资源不存在档案UUID不存在
- 500: 服务器内部错误(数据库异常)
- 删除流程
- 处理器从上下文取出 user_id若缺失则直接返回 401。
- 服务层先查询档案,若不存在返回 404随后校验权限不匹配返回 403。
- 通过权限校验后执行删除,成功返回 200。
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "处理器 : DeleteProfile"
participant S as "服务 : profile_service"
participant P as "仓储 : profile_repository"
participant D as "数据库 : GORM"
C->>H : "DELETE /api/v1/profile/{uuid}"
H->>S : "调用 DeleteProfile(uuid, user_id)"
S->>P : "FindProfileByUUID"
P->>D : "查询"
D-->>P : "返回档案或不存在"
P-->>S : "结果"
alt "档案不存在"
S-->>H : "返回 404"
H-->>C : "404"
else "权限校验失败"
S-->>H : "返回 403"
H-->>C : "403"
else "权限通过"
S->>P : "DeleteProfile"
P->>D : "删除记录"
D-->>P : "成功"
S-->>H : "返回 nil"
H-->>C : "200 成功"
end
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L282-L339)
- [profile_service.go](file://internal/service/profile_service.go#L137-L159)
- [profile_repository.go](file://internal/repository/profile_repository.go#L73-L77)
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L282-L339)
- [profile_service.go](file://internal/service/profile_service.go#L137-L159)
### 权限验证与鉴权中间件
- 中间件职责
- 校验 Authorization 头格式Bearer token
- 使用 JWT 服务验证令牌有效性,并将 user_id、username、role 写入上下文。
- 处理器侧使用
- 处理器从上下文读取 user_id若不存在则返回 401。
- 服务层进一步校验档案归属,不匹配返回 403。
- JWT 服务
- 生成与验证使用 HS256 签名算法,过期时间可配置。
章节来源
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [jwt.go](file://pkg/auth/jwt.go#L10-L71)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
### 事务性保证与数据一致性
- 更新操作
- 当前 UpdateProfile 通过单次 save/update 完成,不涉及跨表事务。
- 若未来扩展为多步更新(例如同时更新多个字段或关联表),建议在服务层使用 GORM 事务包裹,确保原子性与一致性。
- 删除操作
- DeleteProfile 为单条删除,不涉及跨表事务。
- 活跃档案设置
- SetActiveProfile 使用 GORM 事务,先将用户所有档案设为非活跃,再将目标档案设为活跃,保证“同一用户仅有一个活跃档案”的约束。
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L92-L135)
- [profile_service.go](file://internal/service/profile_service.go#L137-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
## 依赖关系分析
- 路由依赖
- profile 路由组使用鉴权中间件,确保后续接口均需有效 JWT。
- 处理器依赖
- profile_handler 依赖鉴权中间件提供的 user_id依赖 service 层执行业务逻辑,依赖 model/types 定义的数据结构。
- 服务层依赖
- profile_service 依赖 repository 层进行数据访问,依赖数据库连接管理器。
- 仓储层依赖
- profile_repository 依赖 GORM 与数据库连接。
- 鉴权与数据库
- jwt.go 提供令牌签发与校验manager.go 提供数据库连接获取与迁移。
```mermaid
graph LR
Routes["routes.go"] --> AuthMW["auth.go"]
AuthMW --> Handler["profile_handler.go"]
Handler --> Service["profile_service.go"]
Service --> Repo["profile_repository.go"]
Repo --> DBMgr["manager.go"]
Handler --> Types["common.go"]
Handler --> Model["profile.go"]
Handler --> JWT["jwt.go"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
- [profile_service.go](file://internal/service/profile_service.go#L92-L159)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L77)
- [manager.go](file://pkg/database/manager.go#L35-L50)
- [common.go](file://internal/types/common.go#L201-L206)
- [profile.go](file://internal/model/profile.go#L7-L29)
- [jwt.go](file://pkg/auth/jwt.go#L10-L71)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
- [profile_service.go](file://internal/service/profile_service.go#L92-L159)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L77)
- [manager.go](file://pkg/database/manager.go#L35-L50)
## 性能考量
- 查询与预加载
- 仓储层在查询档案时使用预加载关联的皮肤与披风,有助于减少 N+1 查询问题,提升响应速度。
- 事务范围
- 当前更新与删除均为单条操作,事务开销较小;若扩展为多步更新,建议将相关操作合并到事务中,避免部分成功导致的不一致。
- 唯一性检查
- 更新名称时进行同名检查,避免并发场景下的重复;建议在数据库层面增加唯一索引以降低竞争条件风险。
- 日志与可观测性
- 处理器在发生错误时记录日志,便于定位问题;建议在服务层也增加关键步骤的日志埋点。
章节来源
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [profile_service.go](file://internal/service/profile_service.go#L92-L135)
## 故障排查指南
- 401 未授权
- 检查请求头 Authorization 是否为 Bearer 令牌格式;确认令牌未过期且签名正确。
- 403 无权操作
- 确认当前用户是否拥有目标档案;检查档案所属 user_id 与当前用户是否一致。
- 404 资源不存在
- 确认档案 UUID 是否正确;检查数据库中是否存在该记录。
- 400 参数错误
- 检查请求体字段是否符合长度与类型要求;例如 name 长度应在 1-16 之间。
- 500 服务器错误
- 查看服务端日志,关注数据库连接、唯一性冲突、事务回滚等问题。
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
- [profile_service.go](file://internal/service/profile_service.go#L92-L159)
## 结论
- PUT /api/v1/profile/:uuid 与 DELETE /api/v1/profile/:uuid 已具备完善的权限校验与存在性验证机制。
- 更新操作支持名称与材质字段的灵活更新,删除操作简洁可靠。
- 服务层与仓储层清晰分离职责,当前更新与删除为单步操作;若未来扩展为多步更新,建议引入事务以保障一致性。
- 建议在数据库层面完善唯一性约束,并在服务层增加关键步骤的日志埋点,以便于问题定位与性能优化。

View File

@@ -1,336 +0,0 @@
# 档案API
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [profile.go](file://internal/model/profile.go)
- [texture.go](file://internal/model/texture.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [postgres.go](file://pkg/database/postgres.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者与集成方系统化梳理“档案API”的设计与使用方法覆盖以下能力
- 档案的创建、查询、更新、删除与“活跃档案”设置
- 公开获取档案详情与需要认证的档案管理端点
- 档案与Minecraft用户UUID的关系、活跃档案的概念
- 档案列表的获取方式
- 完整的API使用示例包括创建档案的请求体结构与获取档案详情的响应格式
- 档案与材质之间的关系
## 项目结构
档案API位于路由组 `/api/v1/profile` 下,采用“公开路由 + 认证路由”的分层设计:
- 公开路由通过档案UUID获取档案详情
- 认证路由需要携带JWT令牌支持创建、查询列表、更新、删除、设置活跃档案
```mermaid
graph TB
subgraph "路由组 /api/v1/profile"
A["GET /:uuid<br/>公开:获取档案详情"]
subgraph "认证组"
B["POST /<br/>创建档案"]
C["GET /<br/>获取我的档案列表"]
D["PUT /:uuid<br/>更新档案"]
E["DELETE /:uuid<br/>删除档案"]
F["POST /:uuid/activate<br/>设置活跃档案"]
end
end
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
## 核心组件
- 路由注册:集中于路由文件,定义公开与认证端点
- 处理器profile_handler 负责鉴权、参数解析、调用服务层并返回统一响应
- 服务层profile_service 实现业务规则如创建档案时生成UUID与RSA私钥、设置活跃状态、权限校验等
- 仓储层profile_repository 封装数据库操作(增删改查、统计、事务)
- 类型与模型types 中定义请求/响应结构model 中定义档案与材质模型及响应结构
- 数据库通过GORM连接PostgreSQL统一日志与连接池配置
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [common.go](file://internal/types/common.go#L81-L207)
- [response.go](file://internal/model/response.go#L1-L86)
- [profile.go](file://internal/model/profile.go#L1-L64)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
## 架构总览
档案API遵循经典的三层架构HTTP路由 -> 处理器 -> 服务 -> 仓储 -> 数据库。认证中间件确保仅持有有效令牌的用户可访问受保护端点。
```mermaid
graph TB
Client["客户端"] --> R["Gin 路由<br/>/api/v1/profile"]
R --> M["认证中间件"]
M --> H["处理器<br/>profile_handler"]
H --> S["服务层<br/>profile_service"]
S --> Repo["仓储层<br/>profile_repository"]
Repo --> DB["数据库<br/>PostgreSQL(GORM)"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
## 详细组件分析
### 路由与端点
- 公开端点
- GET /api/v1/profile/:uuid根据UUID获取档案详情
- 认证端点
- POST /api/v1/profile创建档案
- GET /api/v1/profile获取当前用户的所有档案列表
- PUT /api/v1/profile/:uuid更新档案可更新角色名、皮肤ID、披风ID
- DELETE /api/v1/profile/:uuid删除档案
- POST /api/v1/profile/:uuid/activate设置活跃档案同时将该用户其他档案设为非活跃
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
### 处理器与鉴权
- 认证中间件所有认证端点均使用认证中间件从上下文提取user_id
- 参数绑定使用Gin的ShouldBindJSON进行请求体校验
- 统一响应:使用统一响应结构,错误码与消息标准化
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [response.go](file://internal/model/response.go#L1-L86)
### 服务层业务规则
- 创建档案
- 校验用户存在且状态正常
- 校验角色名唯一
- 生成UUID与RSA私钥PEM格式
- 默认设置为活跃档案,并将该用户其他档案设为非活跃
- 更新档案
- 校验档案归属(仅档案所属用户可更新)
- 校验新角色名唯一
- 支持更新角色名、皮肤ID、披风ID
- 删除档案
- 校验档案归属
- 设置活跃档案
- 校验档案归属
- 使用事务将该用户其他档案设为非活跃,再将目标档案设为活跃
- 同步更新最后使用时间
- 档案数量限制
- 通过服务层检查当前用户档案数量是否超过上限
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
### 仓储层与数据库
- 查询
- FindProfileByUUID按UUID查询并预加载皮肤与披风
- FindProfilesByUserID按用户ID查询并预加载皮肤与披风按创建时间倒序
- FindProfileByName按角色名查询
- 更新
- UpdateProfile、UpdateProfileFields
- 删除
- DeleteProfile按UUID删除
- 统计
- CountProfilesByUserID
- 活跃状态
- SetActiveProfile事务内将该用户其他档案设为非活跃再将目标档案设为活跃
- 最后使用时间
- UpdateProfileLastUsedAt
章节来源
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
### 模型与类型
- 档案模型
- 字段UUID、用户ID、角色名、皮肤ID、披风ID、活跃状态、最后使用时间、创建/更新时间
- 关联User、Skin(Texture)、Cape(Texture)
- 档案响应结构
- ProfileResponse包含UUID、角色名、textures皮肤/披风)、活跃状态、最后使用时间、创建时间
- ProfileTexturesData包含SKIN与CAPE两个可选字段
- ProfileTexture包含URL与可选metadata如模型类型
- 请求/响应类型
- CreateProfileRequestname必填1-16字符
- UpdateProfileRequestname可选、skin_id可选、cape_id可选
- ProfileInfo用于列表与详情的统一返回字段
章节来源
- [profile.go](file://internal/model/profile.go#L1-L64)
- [common.go](file://internal/types/common.go#L81-L207)
### 档案与材质的关系
- 档案可关联两种材质皮肤SkinID与披风CapeID
- 材质模型包含类型SKIN/CAPE、URL、哈希、大小、公开状态等
- 服务层与仓储层均支持对材质的创建、查询、更新、删除、收藏等操作但这些属于独立的材质API范畴
章节来源
- [profile.go](file://internal/model/profile.go#L1-L64)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
### API使用示例
- 创建档案
- 方法与路径POST /api/v1/profile
- 认证需要JWT
- 请求体结构CreateProfileRequest
- name字符串必填长度1-16
- 成功响应返回ProfileInfo包含UUID、用户ID、角色名、皮肤ID、披风ID、活跃状态、最后使用时间、创建/更新时间)
- 常见错误400参数错误/达到档案数量上限、401未授权、500服务器错误
- 获取我的档案列表
- 方法与路径GET /api/v1/profile
- 认证需要JWT
- 成功响应数组元素为ProfileInfo
- 获取档案详情
- 方法与路径GET /api/v1/profile/{uuid}
- 认证公开端点无需JWT
- 成功响应ProfileInfo
- 更新档案
- 方法与路径PUT /api/v1/profile/{uuid}
- 认证需要JWT
- 请求体结构UpdateProfileRequest
- name字符串可选长度1-16
- skin_id整数可选
- cape_id整数可选
- 成功响应ProfileInfo
- 删除档案
- 方法与路径DELETE /api/v1/profile/{uuid}
- 认证需要JWT
- 成功响应:{"message":"删除成功"}
- 设置活跃档案
- 方法与路径POST /api/v1/profile/{uuid}/activate
- 认证需要JWT
- 成功响应:{"message":"设置成功"}
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [common.go](file://internal/types/common.go#L81-L207)
- [response.go](file://internal/model/response.go#L1-L86)
### 活跃档案概念
- 每个用户在同一时刻只能有一个“活跃档案”
- 当创建新档案或切换活跃档案时,系统会将该用户其他档案设为非活跃
- 设置活跃档案会同步更新最后使用时间
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
### 档案列表获取方式
- 通过认证端点 GET /api/v1/profile 获取当前用户的所有档案
- 返回顺序按创建时间倒序
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
- [profile_service.go](file://internal/service/profile_service.go#L83-L90)
- [profile_repository.go](file://internal/repository/profile_repository.go#L44-L57)
### 档案与Minecraft用户UUID的关系
- 档案模型包含UUID字段用于标识Minecraft角色
- 档案与用户通过user_id关联
- 公开端点通过UUID获取档案详情不涉及用户身份
章节来源
- [profile.go](file://internal/model/profile.go#L1-L64)
- [routes.go](file://internal/handler/routes.go#L63-L79)
## 依赖分析
- 路由到处理器:路由文件注册各端点,处理器负责鉴权与参数解析
- 处理器到服务:处理器调用服务层实现业务逻辑
- 服务到仓储:服务层封装业务规则并委托仓储层执行数据库操作
- 仓储到数据库仓储层通过GORM访问PostgreSQL
```mermaid
graph LR
Routes["routes.go"] --> Handler["profile_handler.go"]
Handler --> Service["profile_service.go"]
Service --> Repo["profile_repository.go"]
Repo --> DB["postgres.go"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
## 性能考虑
- 预加载策略查询档案时预加载皮肤与披风减少N+1查询风险
- 分页与排序:列表查询按创建时间倒序,避免全量扫描
- 事务一致性:设置活跃档案使用事务,保证原子性
- 连接池:数据库连接池配置合理,建议结合实际负载调整
章节来源
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L57)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
## 故障排查指南
- 未授权
- 现象返回401
- 排查确认JWT是否正确传递与有效
- 参数错误
- 现象返回400
- 排查:检查请求体字段是否符合长度与类型要求
- 无权操作
- 现象返回403
- 排查:确认操作的档案是否属于当前用户
- 资源不存在
- 现象返回404
- 排查确认UUID是否正确、档案是否被删除
- 达到上限
- 现象返回400
- 排查:检查当前用户档案数量与系统限制
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [response.go](file://internal/model/response.go#L1-L86)
## 结论
档案API以清晰的公开/认证分层设计结合严格的鉴权与参数校验提供了完整的Minecraft角色档案生命周期管理能力。通过服务层的业务规则与仓储层的事务保障系统在一致性与扩展性方面具备良好基础。建议在生产环境中配合限流、缓存与监控进一步优化性能与稳定性。
## 附录
### API端点一览
- 公开
- GET /api/v1/profile/:uuid
- 认证
- POST /api/v1/profile
- GET /api/v1/profile
- PUT /api/v1/profile/:uuid
- DELETE /api/v1/profile/:uuid
- POST /api/v1/profile/:uuid/activate
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)

View File

@@ -1,288 +0,0 @@
# 激活管理
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [profile.go](file://internal/model/profile.go)
- [response.go](file://internal/model/response.go)
- [manager.go](file://pkg/database/manager.go)
- [postgres.go](file://pkg/database/postgres.go)
- [config.go](file://pkg/config/config.go)
- [main.go](file://cmd/server/main.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本文件聚焦于“档案激活管理API”的设计与实现围绕 POST /api/v1/profile/:uuid/activate 端点展开,解释“活跃档案”的概念及其在系统中的作用:代表用户当前在游戏中使用的角色外观。调用该接口后,系统通过数据库事务确保原子性:先将用户所有其他档案的 is_active 字段设为 false再将指定 UUID 的档案设为 active 状态;同时,该操作会更新档案的 last_used_at 时间戳,用于追踪最近使用情况。本文还提供调用示例、成功/失败响应说明,并结合 repository 层的 SetActiveProfile 事务实现,说明数据一致性保障机制。
## 项目结构
该模块位于典型的分层架构中:
- 路由层:定义 API 路由与鉴权中间件绑定
- 处理器层:接收请求、解析参数、调用服务层并输出响应
- 服务层:编排业务流程,进行权限校验与调用仓库层
- 仓库层:封装数据库操作,提供事务与字段更新能力
- 模型层:定义数据结构与表映射
- 数据库层GORM 初始化、连接池与迁移
```mermaid
graph TB
subgraph "路由层"
R["internal/handler/routes.go"]
end
subgraph "处理器层"
H["internal/handler/profile_handler.go"]
end
subgraph "服务层"
S["internal/service/profile_service.go"]
end
subgraph "仓库层"
RP["internal/repository/profile_repository.go"]
end
subgraph "模型层"
M["internal/model/profile.go"]
end
subgraph "数据库层"
DM["pkg/database/manager.go"]
DP["pkg/database/postgres.go"]
end
subgraph "应用入口"
MAIN["cmd/server/main.go"]
end
R --> H
H --> S
S --> RP
RP --> DM
DM --> DP
S --> M
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [manager.go](file://pkg/database/manager.go#L13-L50)
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
- [main.go](file://cmd/server/main.go#L41-L51)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [manager.go](file://pkg/database/manager.go#L13-L50)
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
- [main.go](file://cmd/server/main.go#L41-L51)
## 核心组件
- 路由注册:在路由组 /api/v1/profile 下注册 POST /:uuid/activate绑定鉴权中间件
- 处理器从上下文提取用户ID与UUID调用服务层设置活跃档案
- 服务层:校验档案归属、调用仓库层执行事务设置活跃状态,并更新 last_used_at
- 仓库层:使用 GORM 事务,先批量将用户其他档案置为非活跃,再将目标档案置为活跃;随后更新 last_used_at
- 模型层Profile 结构体包含 uuid、user_id、name、is_active、last_used_at 等字段
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
- [profile.go](file://internal/model/profile.go#L7-L24)
## 架构总览
下图展示了从客户端到数据库的完整调用链路,以及事务原子性保障。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由层<br/>routes.go"
participant H as "处理器层<br/>profile_handler.go"
participant S as "服务层<br/>profile_service.go"
participant RP as "仓库层<br/>profile_repository.go"
participant DB as "数据库(GORM)"
C->>R : "POST /api/v1/profile/ : uuid/activate"
R->>H : "绑定鉴权中间件后进入处理器"
H->>H : "从上下文获取 user_id 并校验"
H->>S : "调用 SetActiveProfile(db, uuid, user_id)"
S->>RP : "FindProfileByUUID(uuid)"
RP-->>S : "返回档案或错误"
S->>S : "校验档案归属(user_id)"
S->>RP : "SetActiveProfile(uuid, user_id) 事务"
RP->>DB : "事务开始"
DB-->>RP : "事务上下文"
RP->>DB : "批量更新其他档案为非活跃"
RP->>DB : "更新目标档案为活跃"
RP-->>S : "提交事务"
S->>RP : "UpdateProfileLastUsedAt(uuid)"
RP->>DB : "更新 last_used_at"
RP-->>S : "返回成功"
S-->>H : "返回成功"
H-->>C : "200 OK {message : 设置成功}"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
## 详细组件分析
### 活跃档案的概念与作用
- 概念:活跃档案是用户当前在游戏中使用的角色外观,系统通过 is_active 字段标识
- 作用:确保同一用户在同一时刻仅有一个活跃档案,避免多角色外观冲突;同时通过 last_used_at 记录最近使用时间,便于审计与统计
章节来源
- [profile.go](file://internal/model/profile.go#L7-L24)
### 接口定义与调用流程
- 方法与路径POST /api/v1/profile/:uuid/activate
- 鉴权:需携带有效 JWT处理器从上下文提取 user_id
- 参数:
- 路径参数 uuid目标档案的唯一标识
- 成功响应200 OK返回统一成功响应结构
- 失败响应:
- 401 未授权:缺少或无效的 JWT
- 403 禁止访问:档案不属于当前用户
- 404 资源不存在:档案不存在
- 500 服务器错误:其他数据库或业务异常
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [response.go](file://internal/model/response.go#L20-L38)
### 处理器层实现要点
- 从上下文获取 user_id若缺失返回 401
- 调用服务层 SetActiveProfile根据错误类型映射为 404 或 403 或 500
- 成功返回 200 OK
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [response.go](file://internal/model/response.go#L20-L38)
### 服务层业务逻辑
- 校验档案是否存在与归属关系
- 调用仓库层 SetActiveProfile 执行事务
- 事务完成后调用 UpdateProfileLastUsedAt 更新 last_used_at
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
### 仓库层事务实现与数据一致性
- 使用 GORM 事务,确保以下两个更新在同一事务中:
1) 将用户所有档案的 is_active 设为 false
2) 将指定 uuid 的档案 is_active 设为 true
- 提交事务后,调用 UpdateProfileLastUsedAt 将 last_used_at 更新为当前时间
- 该实现保证了“同一时刻仅有一个活跃档案”的强一致性
```mermaid
flowchart TD
Start(["进入 SetActiveProfile 事务"]) --> BatchDeactivate["批量更新其他档案为非活跃"]
BatchDeactivate --> TargetActivate["更新目标档案为活跃"]
TargetActivate --> Commit{"事务提交成功?"}
Commit --> |是| UpdateTimestamp["更新 last_used_at"]
Commit --> |否| Rollback["回滚事务"]
UpdateTimestamp --> End(["返回成功"])
Rollback --> End
```
图表来源
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
章节来源
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
### 数据模型与字段说明
- Profile 模型包含:
- uuid档案唯一标识
- user_id所属用户ID
- name角色名
- is_active是否为活跃档案
- last_used_at最后使用时间
- created_at/updated_at创建与更新时间
章节来源
- [profile.go](file://internal/model/profile.go#L7-L24)
### 数据库连接与迁移
- 应用启动时初始化数据库连接与 GORM 实例,执行 AutoMigrate 自动迁移
- 连接池参数可配置PostgreSQL 驱动通过 DSN 构建
- Profile 表在迁移中创建,包含上述字段及索引
章节来源
- [main.go](file://cmd/server/main.go#L41-L51)
- [manager.go](file://pkg/database/manager.go#L13-L50)
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
- [config.go](file://pkg/config/config.go#L34-L47)
## 依赖关系分析
- 路由层依赖处理器层
- 处理器层依赖服务层
- 服务层依赖仓库层与模型层
- 仓库层依赖数据库管理器与 GORM
- 应用入口负责初始化数据库并执行迁移
```mermaid
graph LR
Routes["routes.go"] --> Handler["profile_handler.go"]
Handler --> Service["profile_service.go"]
Service --> Repo["profile_repository.go"]
Repo --> DBMgr["database/manager.go"]
DBMgr --> PG["database/postgres.go"]
Main["cmd/server/main.go"] --> DBMgr
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
- [manager.go](file://pkg/database/manager.go#L13-L50)
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
- [main.go](file://cmd/server/main.go#L41-L51)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
- [manager.go](file://pkg/database/manager.go#L13-L50)
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
- [main.go](file://cmd/server/main.go#L41-L51)
## 性能考量
- 事务内两次 UPDATE 操作,均基于 user_id 与 uuid 的过滤条件,建议在 user_id 上建立索引以提升批量更新效率
- last_used_at 更新为单行更新,影响范围小
- 数据库连接池参数可通过配置调整,以平衡并发与资源占用
[本节为通用性能建议,不直接分析具体文件]
## 故障排查指南
- 401 未授权:确认请求头携带有效 JWT且令牌未过期
- 403 禁止访问:目标档案不属于当前用户,检查 uuid 对应的 user_id
- 404 资源不存在uuid 无效或档案已被删除
- 500 服务器错误:数据库异常或服务层内部错误,查看日志定位具体原因
- 事务一致性问题:若出现“多个活跃档案”,检查数据库索引与事务提交逻辑
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
## 结论
POST /api/v1/profile/:uuid/activate 端点通过严格的鉴权与事务控制,确保“同一用户仅有一个活跃档案”的数据一致性,并在成功后更新 last_used_at。该设计既满足业务需求又通过数据库层的事务保障了原子性与可靠性。建议在生产环境中关注索引与连接池配置以进一步优化性能与稳定性。

View File

@@ -1,299 +0,0 @@
# 详情查询
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [profile.go](file://internal/model/profile.go)
- [texture.go](file://internal/model/texture.go)
- [response.go](file://internal/model/response.go)
- [common.go](file://internal/types/common.go)
- [postgres.go](file://pkg/database/postgres.go)
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本文件面向“详情查询”API聚焦 GET /api/v1/profile/:uuid 端点。该接口为公开路由无需认证即可访问用于根据Minecraft用户UUID获取档案的公开信息。响应体包含档案UUID、用户名、创建时间、最后使用时间、活跃状态以及关联的皮肤和披风信息若存在。该接口在Minecraft客户端通过Yggdrasil协议连接服务器时可被用于查询玩家档案信息以支持皮肤/披风展示等场景。
## 项目结构
围绕“详情查询”API的关键文件组织如下
- 路由注册:在路由层定义公开的 GET /api/v1/profile/:uuid并将其挂载到 v1 分组下。
- 处理器:实现 GetProfile 处理函数,负责参数解析、调用服务层、构造统一响应。
- 服务层:封装业务逻辑,调用仓库层进行数据查询。
- 仓库层基于GORM执行数据库查询预加载皮肤/披风关联。
- 模型与类型:定义档案模型、响应结构、纹理类型等。
- 数据库PostgreSQL驱动与连接池配置。
```mermaid
graph TB
subgraph "路由层"
R["routes.go<br/>注册 /api/v1/profile/:uuid"]
end
subgraph "处理器层"
H["profile_handler.go<br/>GetProfile 处理函数"]
end
subgraph "服务层"
S["profile_service.go<br/>GetProfileByUUID"]
end
subgraph "仓库层"
REPO["profile_repository.go<br/>FindProfileByUUID"]
end
subgraph "模型与类型"
M["profile.go<br/>Profile/ProfileResponse"]
T["texture.go<br/>Texture"]
RESP["response.go<br/>统一响应结构"]
TYPES["common.go<br/>ProfileInfo"]
end
subgraph "数据库"
DB["postgres.go<br/>PostgreSQL 连接与配置"]
end
R --> H
H --> S
S --> REPO
REPO --> DB
H --> RESP
S --> M
REPO --> M
M --> T
H --> TYPES
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [response.go](file://internal/model/response.go#L1-L86)
- [common.go](file://internal/types/common.go#L154-L165)
- [postgres.go](file://pkg/database/postgres.go#L13-L74)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
## 核心组件
- 路由层:在 v1 分组下注册 GET /api/v1/profile/:uuid明确该端点为公开路由无需JWT认证。
- 处理器层GetProfile 从路径参数读取 uuid调用服务层查询档案构造统一响应。
- 服务层GetProfileByUUID 调用仓库层查询档案;若未找到返回“档案不存在”错误。
- 仓库层FindProfileByUUID 使用 uuid 条件查询,并预加载 Skin 和 Cape 关联。
- 模型与类型Profile 定义档案字段及关联ProfileResponse 为对外响应结构ProfileInfo 为处理器侧返回结构Texture 定义材质模型及其索引字段。
- 统一响应response.go 提供统一的 Response/Error 结构,便于前后端约定。
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [response.go](file://internal/model/response.go#L1-L86)
- [common.go](file://internal/types/common.go#L154-L165)
## 架构总览
下面的序列图展示了客户端调用 GET /api/v1/profile/:uuid 的典型流程以及与Yggdrasil协议的集成场景。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Router as "Gin 路由"
participant Handler as "GetProfile 处理器"
participant Service as "profile_service.GetProfileByUUID"
participant Repo as "profile_repository.FindProfileByUUID"
participant DB as "PostgreSQL"
Client->>Router : "GET /api/v1/profile/ : uuid"
Router->>Handler : "路由分发"
Handler->>Service : "GetProfileByUUID(uuid)"
Service->>Repo : "FindProfileByUUID(uuid)"
Repo->>DB : "SELECT ... WHERE uuid=?<br/>并预加载 Skin/Cape"
DB-->>Repo : "Profile 记录"
Repo-->>Service : "Profile 对象"
Service-->>Handler : "Profile 对象"
Handler-->>Client : "200 + 统一响应体"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [postgres.go](file://pkg/database/postgres.go#L13-L74)
## 详细组件分析
### 接口定义与行为
- 路由:公开端点 GET /api/v1/profile/:uuid无需认证。
- 请求参数:路径参数 uuidMinecraft档案UUID
- 成功响应:返回统一响应结构,包含档案基本信息与关联皮肤/披风信息。
- 错误响应当档案不存在时返回404。
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [response.go](file://internal/model/response.go#L1-L86)
### 响应数据结构
- 响应体字段(对外):
- uuid档案UUID
- nameMinecraft角色名
- textures包含皮肤与披风信息的对象
- SKIN皮肤信息可选
- url皮肤图片地址
- metadata元数据可选
- model皮肤模型slim 或 classic
- CAPE披风信息可选
- url披风图片地址
- metadata元数据可选
- is_active是否为活跃档案
- last_used_at最后使用时间可选
- created_at创建时间
- updated_at更新时间
- 处理器侧返回结构ProfileInfo
- uuid、user_id、name、skin_id、cape_id、is_active、last_used_at、created_at、updated_at
章节来源
- [profile.go](file://internal/model/profile.go#L31-L57)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [common.go](file://internal/types/common.go#L154-L165)
### 查询流程与错误码
- 正常流程:处理器读取 uuid -> 服务层查询 -> 仓库层查询并预加载关联 -> 返回统一响应。
- 错误码:
- 404档案不存在
- 500服务器内部错误数据库异常、服务层错误等
```mermaid
flowchart TD
Start(["进入 GetProfile"]) --> Parse["解析路径参数 uuid"]
Parse --> Query["调用服务层 GetProfileByUUID"]
Query --> RepoCall["仓库层 FindProfileByUUID(uuid)"]
RepoCall --> Found{"找到记录?"}
Found --> |是| BuildResp["组装响应含 textures"]
Found --> |否| NotFound["返回 404 档案不存在"]
BuildResp --> Ok["返回 200 + 统一响应"]
NotFound --> End(["结束"])
Ok --> End
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [response.go](file://internal/model/response.go#L1-L86)
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [response.go](file://internal/model/response.go#L1-L86)
### 与Yggdrasil协议的集成场景
- 在Minecraft客户端连接服务器时可通过Yggdrasil的会话服务器端点获取玩家档案信息从而展示皮肤/披风。
- 该端点与Yggdrasil的会话服务器端点存在语义上的互补前者为公开档案详情查询后者为认证后的会话信息查询。
- 典型调用链:客户端 -> 服务器Yggdrasil-> 本系统 GetProfileByUUID公开详情查询
章节来源
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L426-L440)
## 依赖分析
- 路由层依赖 Gin 路由注册,将公开端点挂载至 v1 分组。
- 处理器层依赖服务层与统一响应结构。
- 服务层依赖仓库层与数据库连接。
- 仓库层依赖 GORM 与 PostgreSQL 驱动。
- 模型层定义档案与纹理的字段、索引与关联。
```mermaid
graph LR
Routes["routes.go"] --> Handler["profile_handler.go"]
Handler --> Service["profile_service.go"]
Service --> Repo["profile_repository.go"]
Repo --> DB["postgres.go"]
Handler --> Resp["response.go"]
Service --> Model["profile.go"]
Repo --> Model
Model --> Texture["texture.go"]
Handler --> Types["common.go"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [postgres.go](file://pkg/database/postgres.go#L13-L74)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [response.go](file://internal/model/response.go#L1-L86)
- [common.go](file://internal/types/common.go#L154-L165)
章节来源
- [routes.go](file://internal/handler/routes.go#L63-L79)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [postgres.go](file://pkg/database/postgres.go#L13-L74)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [response.go](file://internal/model/response.go#L1-L86)
- [common.go](file://internal/types/common.go#L154-L165)
## 性能考量
- 数据库索引与查询优化
- Profile 表的 uuid 字段为主键,查询命中主键索引,具备高效率。
- Profile 表的 name 字段具有唯一索引,有助于去重与唯一性约束。
- Profile 表的 user_id 字段具有普通索引,有利于按用户维度查询。
- Profile 表的 is_active 字段具有索引,便于筛选活跃档案。
- Texture 表的 hash 字段具有唯一索引,有利于快速定位材质。
- Texture 表的 uploader_id、is_public、favorite_count、download_count 等字段具有索引,便于检索与排序。
- 关联预加载
- 仓库层在查询档案时使用预加载Preload加载 Skin 与 Cape减少 N+1 查询风险,提升响应速度。
- 数据库连接池
- PostgreSQL 驱动初始化时配置了连接池参数(最大空闲连接、最大打开连接、连接最大生命周期),有助于并发场景下的稳定性与吞吐量。
- 缓存建议
- 对高频查询的档案详情可引入缓存如Redis以进一步降低数据库压力。
- 缓存键可采用 profile:{uuid},并设置合理过期时间(如几分钟)。
- 日志与监控
- 处理器层记录错误日志,便于定位慢查询与异常。
- 建议增加指标埋点如QPS、P95/P99延迟、错误率以便持续优化。
章节来源
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
- [profile.go](file://internal/model/profile.go#L7-L24)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [postgres.go](file://pkg/database/postgres.go#L13-L74)
## 故障排查指南
- 404 档案不存在
- 现象:请求返回 404消息提示“档案不存在”。
- 排查:确认 uuid 是否正确;检查数据库中是否存在该 uuid 的档案记录。
- 500 服务器内部错误
- 现象:请求返回 500消息提示服务层或仓库层错误。
- 排查:查看处理器层日志;检查数据库连接与连接池配置;确认 GORM 查询是否报错。
- 响应缺少皮肤/披风信息
- 现象textures 字段为空或部分缺失。
- 排查:确认档案是否绑定了皮肤/披风;检查关联表是否存在有效记录;确认仓库层预加载是否生效。
- Yggdrasil集成问题
- 现象:客户端无法显示皮肤/披风。
- 排查确认客户端调用的Yggdrasil端点与本系统公开详情查询端点是否一致核对响应结构与字段命名是否匹配。
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
- [response.go](file://internal/model/response.go#L1-L86)
## 结论
GET /api/v1/profile/:uuid 作为公开端点提供了基于Minecraft档案UUID的详情查询能力。其架构清晰、职责分明路由层公开、处理器层编排、服务层封装、仓库层持久化、模型层定义。响应体包含档案基础信息与皮肤/披风信息满足客户端在Yggdrasil协议下的集成需求。通过合理的数据库索引、关联预加载与连接池配置可在高并发场景下保持稳定与高效。建议结合缓存与监控体系持续优化查询性能与用户体验。

View File

@@ -1,357 +0,0 @@
# 个人资料管理
<cite>
**本文引用的文件**
- [user_handler.go](file://internal/handler/user_handler.go)
- [routes.go](file://internal/handler/routes.go)
- [auth.go](file://internal/middleware/auth.go)
- [user_service.go](file://internal/service/user_service.go)
- [user_repository.go](file://internal/repository/user_repository.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [user.go](file://internal/model/user.go)
- [jwt.go](file://pkg/auth/jwt.go)
- [password.go](file://pkg/auth/password.go)
- [user_handler_test.go](file://internal/handler/user_handler_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者与测试人员系统性梳理“个人资料管理”相关API重点覆盖
- GET /api/v1/user/profile获取当前登录用户的详细信息
- PUT /api/v1/user/profile更新头像与密码邮箱修改请使用独立接口
文档将解释 UserInfo 数据结构各字段及 JSON 序列化规则;详述密码更新的安全机制(旧密码校验与新密码加密);说明头像 URL 的更新流程(预签名上传 URL 生成与最终更新)。同时,结合 user_handler.go 中的 GetUserProfile 与 UpdateUserProfile 函数,说明 JWT 中间件如何注入用户上下文,服务层如何调用仓库层执行持久化操作,并提供完整请求/响应示例(含成功与常见错误场景)。
## 项目结构
围绕个人资料管理的代码组织遵循“控制器-中间件-服务-仓库-模型”的分层设计,路由在 v1 组下统一挂载,用户相关接口均受 JWT 认证保护。
```mermaid
graph TB
Client["客户端"] --> Router["Gin 路由器"]
Router --> GroupV1["/api/v1 用户组<br/>启用 AuthMiddleware()"]
GroupV1 --> GetUserProfile["GET /api/v1/user/profile"]
GroupV1 --> UpdateUserProfile["PUT /api/v1/user/profile"]
GroupV1 --> AvatarUploadURL["POST /api/v1/user/avatar/upload-url"]
GroupV1 --> UpdateAvatar["PUT /api/v1/user/avatar"]
GetUserProfile --> Handler["user_handler.GetUserProfile"]
UpdateUserProfile --> Handler
AvatarUploadURL --> Handler
UpdateAvatar --> Handler
Handler --> Middleware["AuthMiddleware()<br/>注入 user_id/username/role"]
Handler --> Service["user_service.*"]
Service --> Repo["user_repository.*"]
Service --> Model["model.User"]
Handler --> Types["types.UserInfo / UpdateUserRequest"]
Handler --> Resp["model.Response / model.ErrorResponse"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L16-L41)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
- [user_service.go](file://internal/service/user_service.go#L124-L164)
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
- [common.go](file://internal/types/common.go#L42-L47)
- [response.go](file://internal/model/response.go#L1-L86)
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L41)
## 核心组件
- 控制器层Handler
- GetUserProfile从上下文提取 user_id查询用户并返回 UserInfo
- UpdateUserProfile接收 UpdateUserRequest按需更新密码与头像返回最新 UserInfo
- 中间件层AuthMiddleware
- 解析 Authorization: Bearer <token>,校验 JWT 并将用户信息写入上下文
- 服务层Service
- GetUserByID封装仓库查询
- UpdateUserInfo / UpdateUserAvatar封装仓库更新
- ChangeUserPassword校验旧密码并加密新密码后更新
- 仓库层Repository
- FindUserByID / UpdateUser / UpdateUserFields数据库读写
- 类型与模型
- types.UpdateUserRequest请求体定义
- types.UserInfo响应体定义
- model.User数据库映射模型
- model.Response / model.ErrorResponse统一响应结构
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [user_service.go](file://internal/service/user_service.go#L124-L164)
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
- [common.go](file://internal/types/common.go#L42-L47)
- [user.go](file://internal/model/user.go#L7-L21)
- [response.go](file://internal/model/response.go#L1-L86)
## 架构总览
以下序列图展示“获取/更新用户资料”的端到端流程,包含 JWT 中间件、控制器、服务与仓库的交互。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "Gin 路由"
participant M as "AuthMiddleware"
participant H as "user_handler"
participant S as "user_service"
participant RP as "user_repository"
participant DB as "数据库"
C->>R : "GET /api/v1/user/profile"
R->>M : "鉴权中间件"
M-->>R : "注入 user_id/username/role"
R->>H : "GetUserProfile"
H->>S : "GetUserByID(user_id)"
S->>RP : "FindUserByID(user_id)"
RP->>DB : "SELECT ..."
DB-->>RP : "User"
RP-->>S : "User"
S-->>H : "User"
H-->>C : "200 + UserInfo"
C->>R : "PUT /api/v1/user/profile"
R->>M : "鉴权中间件"
M-->>R : "注入 user_id/username/role"
R->>H : "UpdateUserProfile"
H->>H : "解析请求体 UpdateUserRequest"
alt "更新密码"
H->>S : "ChangeUserPassword(user_id, old, new)"
S->>RP : "FindUserByID(user_id)"
RP->>DB : "SELECT ..."
DB-->>RP : "User"
RP-->>S : "User"
S->>S : "CheckPassword(旧密码)"
S->>S : "HashPassword(新密码)"
S->>RP : "UpdateUserFields(user_id, {password})"
RP->>DB : "UPDATE ..."
DB-->>RP : "OK"
end
alt "更新头像"
H->>S : "UpdateUserInfo(User)"
S->>RP : "UpdateUser(User)"
RP->>DB : "SAVE ..."
DB-->>RP : "OK"
end
H->>S : "GetUserByID(user_id)"
S->>RP : "FindUserByID(user_id)"
RP->>DB : "SELECT ..."
DB-->>RP : "User"
RP-->>S : "User"
S-->>H : "User"
H-->>C : "200 + 最新 UserInfo"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L27-L41)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
- [user_service.go](file://internal/service/user_service.go#L124-L164)
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
## 详细组件分析
### GET /api/v1/user/profile获取当前用户资料
- 功能概述
- 仅限已登录用户访问,通过 Authorization: Bearer <token> 进行鉴权
- 中间件将 user_id 写入上下文,控制器据此查询用户并返回 UserInfo
- 请求与响应
- 请求Authorization: Bearer <token>
- 成功响应200 + model.Response{ data: types.UserInfo }
- 常见错误401 未授权、404 用户不存在
- 数据结构与序列化
- types.UserInfo 字段与 JSON 映射规则详见“附录-数据结构”
- 错误处理
- 未携带或无效的 Authorization 头:返回 401
- 查询不到用户:返回 404
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L17-L68)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [routes.go](file://internal/handler/routes.go#L27-L41)
- [common.go](file://internal/types/common.go#L113-L125)
- [response.go](file://internal/model/response.go#L1-L86)
### PUT /api/v1/user/profile更新头像与密码
- 功能概述
- 支持同时更新头像 URL 与密码;若仅更新头像,可不提供密码字段
- 密码更新安全机制:必须同时提供旧密码与新密码,服务层校验旧密码并通过 bcrypt 加密新密码后更新
- 头像更新流程:先生成预签名上传 URL上传完成后调用更新头像接口最终返回最新 UserInfo
- 请求体 UpdateUserRequest
- avatar可选头像 URL
- old_password可选修改密码时必填
- new_password可选修改密码时必填
- 安全机制与流程
- 旧密码校验:服务层通过 bcrypt 校验
- 新密码加密:服务层使用 bcrypt 生成哈希
- 头像更新:控制器直接更新用户记录;若仅更新头像,服务层保存用户对象
- 错误处理
- 未授权401
- 参数错误400如仅提供新密码或仅提供旧密码
- 用户不存在404
- 服务器错误500
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L70-L193)
- [user_service.go](file://internal/service/user_service.go#L141-L164)
- [password.go](file://pkg/auth/password.go#L1-L21)
- [common.go](file://internal/types/common.go#L42-L47)
- [response.go](file://internal/model/response.go#L27-L53)
### 头像上传与更新流程(补充说明)
- 生成预签名上传 URL
- POST /api/v1/user/avatar/upload-url
- 输入file_name
- 输出post_url、form_data、avatar_url、expires_in
- 更新头像 URL
- PUT /api/v1/user/avatar
- 输入avatar_url查询参数
- 输出:最新 UserInfo
- 注意事项
- 该流程与“更新资料”接口不同,前者用于生成上传凭证,后者用于将最终 URL 写入数据库
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [routes.go](file://internal/handler/routes.go#L34-L39)
- [common.go](file://internal/types/common.go#L68-L80)
### UserInfo 数据结构与 JSON 序列化规则
- 字段说明
- id用户唯一标识
- username用户名
- email邮箱
- avatar头像 URL
- points积分
- role角色如 user
- status账户状态1 正常0 禁用,-1 删除)
- last_login_at最近登录时间可空
- created_at / updated_at创建与更新时间
- JSON 映射
- model.User 中的 Password 字段在 JSON 中不返回(避免泄露)
- 其他字段按 gorm 标签映射到 JSON
章节来源
- [user.go](file://internal/model/user.go#L7-L21)
- [common.go](file://internal/types/common.go#L113-L125)
### 请求/响应示例(含错误场景)
- 获取资料(成功)
- 请求Authorization: Bearer <token>
- 响应200 + { code: 200, message: "操作成功", data: { id, username, email, avatar, points, role, status, last_login_at, created_at, updated_at } }
- 更新资料(仅更新头像)
- 请求体:{ avatar: "https://example.com/new-avatar.png" }
- 响应200 + 最新 UserInfo
- 更新资料(仅更新密码)
- 请求体:{ old_password: "...", new_password: "..." }
- 响应200 + 最新 UserInfo
- 更新资料(参数错误)
- 请求体:{ new_password: "..." }(缺少 old_password
- 响应400 + { code: 400, message: "请求参数错误", error: "修改密码需要提供原密码" }
- 未授权
- 请求:未携带或无效 Authorization
- 响应401 + { code: 401, message: "未授权,请先登录" }
- 用户不存在
- 响应404 + { code: 404, message: "资源不存在" }
- 服务器错误
- 响应500 + { code: 500, message: "服务器内部错误" }
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
- [response.go](file://internal/model/response.go#L27-L53)
- [user_handler_test.go](file://internal/handler/user_handler_test.go#L52-L106)
## 依赖分析
- 控制器依赖
- user_handler 依赖 gin 上下文中的 user_id来自 AuthMiddleware
- 控制器调用 user_service 的 GetUserByID、UpdateUserInfo、ChangeUserPassword、UpdateUserAvatar
- 服务层依赖
- user_service 依赖 user_repository 的 FindUserByID、UpdateUser、UpdateUserFields
- 使用 pkg/auth 的 HashPassword 与 CheckPassword
- 中间件依赖
- AuthMiddleware 依赖 pkg/auth/jwt 的 ValidateToken将 claims 写入上下文
- 模型与类型
- model.User 作为 ORM 映射
- types.UserInfo / UpdateUserRequest 作为 API 层数据契约
```mermaid
graph LR
Handler["user_handler"] --> Service["user_service"]
Handler --> Middleware["AuthMiddleware"]
Handler --> Types["types.*"]
Handler --> Resp["model.Response / ErrorResponse"]
Service --> Repo["user_repository"]
Service --> Auth["pkg/auth/*"]
Middleware --> JWT["pkg/auth/jwt"]
Repo --> Model["model.User"]
```
图表来源
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
- [user_service.go](file://internal/service/user_service.go#L124-L164)
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [password.go](file://pkg/auth/password.go#L1-L21)
- [common.go](file://internal/types/common.go#L42-L47)
- [response.go](file://internal/model/response.go#L1-L86)
- [user.go](file://internal/model/user.go#L7-L21)
## 性能考虑
- 数据库查询
- GetUserByID 使用精确主键查询,复杂度 O(1),建议保持索引完善
- 密码处理
- bcrypt 默认成本较高,建议在高并发场景关注 CPU 开销;可通过配置调整成本值
- 缓存策略
- 对频繁读取的用户资料可在应用层引入缓存(如 Redis减少数据库压力
- 日志与可观测性
- 控制器与服务层已记录关键错误日志,建议配合链路追踪与指标监控
## 故障排查指南
- 401 未授权
- 检查 Authorization 头是否为 Bearer <token> 格式
- 确认 token 未过期且签名正确
- 400 参数错误
- 密码更新时必须同时提供 old_password 与 new_password
- 头像 URL 必须为合法 URL
- 404 用户不存在
- 确认 user_id 是否有效;检查用户状态是否被禁用或删除
- 500 服务器错误
- 检查数据库连接与事务执行情况;查看服务层日志定位具体异常
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L27-L193)
- [user_handler_test.go](file://internal/handler/user_handler_test.go#L108-L152)
- [response.go](file://internal/model/response.go#L27-L53)
## 结论
个人资料管理 API 采用清晰的分层架构JWT 中间件负责鉴权并将用户上下文注入控制器,控制器协调服务层完成业务逻辑,服务层通过仓库层与数据库交互。密码更新具备严格的旧密码校验与新密码加密流程,头像更新支持预签名上传与最终 URL 写入。通过统一的响应结构与完善的错误处理,系统在安全性与易用性之间取得平衡。
## 附录
### 数据结构与序列化规则
- types.UserInfo
- 字段id、username、email、avatar、points、role、status、last_login_at、created_at、updated_at
- JSON 映射:与 model.User 字段一致,除 password 不返回
- types.UpdateUserRequest
- 字段avatar、old_password、new_password
- 校验:密码更新时 old_password 与 new_password 必须同时提供
- model.User数据库映射
- 字段id、username、password、email、avatar、points、role、status、properties、last_login_at、created_at、updated_at
- JSONpassword 不返回;其他字段按标签映射
- model.Response / model.ErrorResponse
- 统一响应结构,包含 code、message、data/error
章节来源
- [common.go](file://internal/types/common.go#L42-L47)
- [common.go](file://internal/types/common.go#L113-L125)
- [user.go](file://internal/model/user.go#L7-L21)
- [response.go](file://internal/model/response.go#L1-L86)

View File

@@ -1,337 +0,0 @@
# 头像管理
<cite>
**本文引用的文件**
- [internal/handler/routes.go](file://internal/handler/routes.go)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go)
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
- [internal/service/user_service.go](file://internal/service/user_service.go)
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
- [pkg/config/config.go](file://pkg/config/config.go)
- [internal/types/common.go](file://internal/types/common.go)
- [internal/model/user.go](file://internal/model/user.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者与运维人员,完整说明“头像管理”功能的实现与使用,包括:
- 生成头像上传URL的APIPOST /api/v1/user/avatar/upload-url
- 更新头像URL的APIPUT /api/v1/user/avatar
- 客户端直传对象存储MinIO/RustFS的完整流程
- upload_service与storage包的协作机制
- 请求参数、返回字段、错误处理与900秒15分钟有效期说明
- 提供可直接参考的curl示例
## 项目结构
围绕头像管理的关键文件组织如下:
- 路由注册在路由层注册两个端点分别对应生成上传URL与更新头像URL
- 处理器:用户处理器包含两个方法,分别处理上述两个端点
- 服务层upload_service负责生成预签名URLuser_service负责更新数据库中的头像URL
- 存储层storage封装了S3兼容客户端提供预签名POST策略生成与对象访问URL构造
- 配置层RustFS配置用于对象存储连接参数与桶映射
- 类型定义:请求与响应结构体定义
- 模型:用户模型包含头像字段
```mermaid
graph TB
Routes["路由注册<br/>/api/v1/user/avatar/upload-url<br/>/api/v1/user/avatar"] --> Handler["用户处理器"]
Handler --> UploadSvc["upload_service<br/>生成头像上传URL"]
Handler --> UserSvc["user_service<br/>更新头像URL"]
UploadSvc --> Storage["storage<br/>预签名POST策略生成"]
Storage --> Config["RustFS配置<br/>endpoint/buckets/use_ssl"]
UserSvc --> Model["用户模型<br/>avatar字段"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L52-L121)
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
- [internal/service/user_service.go](file://internal/service/user_service.go#L134-L140)
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
## 核心组件
- 路由注册在v1用户组下注册头像相关端点均受JWT认证保护
- 生成上传URL处理器接收请求体中的文件名调用服务层生成预签名POST策略与表单数据
- 更新头像URL处理器接收查询参数中的头像URL调用服务层更新数据库
- upload_service校验文件名、选择头像配置、生成对象键、调用storage生成预签名POST策略
- storage封装S3兼容客户端生成预签名POST URL与表单数据并构造最终访问URL
- user_service更新用户头像字段
- 配置RustFS配置包含endpoint、use_ssl、buckets映射
- 类型定义:请求与响应结构体
- 模型:用户实体包含头像字段
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L52-L121)
- [internal/service/user_service.go](file://internal/service/user_service.go#L134-L140)
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
- [internal/types/common.go](file://internal/types/common.go#L68-L79)
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
## 架构总览
头像上传采用“服务端签发、客户端直传”的模式:
- 服务端生成预签名POST策略包含目标桶、对象键、有效期、内容长度范围
- 客户端使用返回的PostURL与FormData直接上传至对象存储
- 上传完成后客户端通知服务端更新数据库中的头像URL
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "用户处理器"
participant S as "upload_service"
participant ST as "storage"
participant FS as "对象存储(RustFS/MinIO)"
participant U as "user_service"
participant DB as "数据库"
C->>H : "POST /api/v1/user/avatar/upload-url"<br/>请求体 : {file_name}
H->>S : "GenerateAvatarUploadURL(userID, file_name)"
S->>ST : "GeneratePresignedPostURL(bucket, objectName, limits, expires)"
ST-->>S : "返回 {post_url, form_data, file_url}"
S-->>H : "返回预签名结果"
H-->>C : "返回 {post_url, form_data, avatar_url, expires_in}"
Note over C,FS : "客户端使用post_url与form_data直接上传到FS"
C->>FS : "HTTP POST 到 post_url + form_data"
FS-->>C : "上传成功"
C->>H : "PUT /api/v1/user/avatar?avatar_url={final_url}"
H->>U : "UpdateUserAvatar(userID, avatar_url)"
U->>DB : "UPDATE user SET avatar = ? WHERE id = ?"
DB-->>U : "OK"
U-->>H : "OK"
H-->>C : "返回最新用户信息"
```
图表来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
- [internal/service/user_service.go](file://internal/service/user_service.go#L134-L140)
## 详细组件分析
### 组件A生成头像上传URLPOST /api/v1/user/avatar/upload-url
- 请求参数
- 请求体包含文件名file_name
- 认证需要JWT
- 处理流程
- 从上下文提取user_id
- 绑定请求体调用upload_service.GenerateAvatarUploadURL
- 生成预签名POST策略与表单数据
- 返回post_url、form_data、avatar_url最终访问URL、expires_in
- 关键点
- 文件名校验:仅允许特定扩展名,且不能为空
- 对象键规则user_{userID}/timestamp_{originalFileName}
- URL有效期15分钟900秒
- 存储桶通过RustFS配置的buckets映射获取avatars桶
```mermaid
flowchart TD
Start(["进入处理器"]) --> Bind["绑定请求体<br/>file_name"]
Bind --> CallSvc["调用upload_service.GenerateAvatarUploadURL"]
CallSvc --> Validate["校验文件名<br/>扩展名与非空"]
Validate --> Bucket["获取avatars桶名"]
Bucket --> ObjKey["生成对象键<br/>user_{userID}/timestamp_{fileName}"]
ObjKey --> Policy["生成预签名POST策略<br/>含有效期与大小限制"]
Policy --> Return["返回post_url/form_data/avatar_url/expires_in"]
Return --> End(["结束"])
```
图表来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L253)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
- [internal/types/common.go](file://internal/types/common.go#L68-L79)
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L253)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L52-L121)
- [internal/types/common.go](file://internal/types/common.go#L68-L79)
### 组件B更新头像URLPUT /api/v1/user/avatar
- 请求参数
- 查询参数avatar_url头像最终访问URL
- 认证需要JWT
- 处理流程
- 从上下文提取user_id
- 校验avatar_url非空
- 调用user_service.UpdateUserAvatar更新数据库
- 返回最新用户信息
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "用户处理器"
participant U as "user_service"
participant DB as "数据库"
C->>H : "PUT /api/v1/user/avatar?avatar_url=..."
H->>H : "校验avatar_url非空"
H->>U : "UpdateUserAvatar(userID, avatar_url)"
U->>DB : "UPDATE user SET avatar = ? WHERE id = ?"
DB-->>U : "OK"
U-->>H : "OK"
H-->>C : "返回最新用户信息"
```
图表来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L255-L326)
- [internal/service/user_service.go](file://internal/service/user_service.go#L134-L140)
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L255-L326)
- [internal/service/user_service.go](file://internal/service/user_service.go#L134-L140)
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
### 组件Cupload_service与storage协作
- upload_service.GenerateAvatarUploadURL
- 校验文件名(扩展名与非空)
- 获取头像上传配置(允许扩展名、最小/最大大小、有效期)
- 解析avatars桶名
- 生成对象键(带时间戳)
- 调用storage.GeneratePresignedPostURL生成预签名POST策略与表单数据
- storage.StorageClient.GeneratePresignedPostURL
- 构建上传策略(桶、键、过期时间、内容长度范围)
- 生成post_url与form_data
- 构造最终访问URL基于endpoint、use_ssl、bucket、objectName
```mermaid
classDiagram
class UploadService {
+GenerateAvatarUploadURL(ctx, storageClient, cfg, userID, fileName) PresignedPostPolicyResult
+ValidateFileName(fileName, fileType) error
+GetUploadConfig(fileType) UploadConfig
}
class StorageClient {
+GeneratePresignedPostURL(ctx, bucketName, objectName, minSize, maxSize, expires, useSSL, endpoint) PresignedPostPolicyResult
+GetBucket(name) string
}
class RustFSConfig {
+Endpoint string
+UseSSL bool
+Buckets map[string]string
}
UploadService --> StorageClient : "调用"
UploadService --> RustFSConfig : "读取配置"
```
图表来源
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
章节来源
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
## 依赖分析
- 路由层依赖处理器层
- 处理器层依赖服务层与配置层
- 服务层依赖存储层与配置层
- 存储层依赖RustFS配置与S3兼容SDK
- 类型定义与模型为上层提供契约
```mermaid
graph LR
Routes["routes.go"] --> Handler["user_handler.go"]
Handler --> UploadSvc["upload_service.go"]
Handler --> UserSvc["user_service.go"]
UploadSvc --> Storage["minio.go"]
UploadSvc --> Cfg["config.go"]
UserSvc --> Model["user.go"]
Handler --> Types["common.go"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [internal/service/user_service.go](file://internal/service/user_service.go#L134-L140)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L52-L121)
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
- [internal/types/common.go](file://internal/types/common.go#L68-L79)
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [internal/service/user_service.go](file://internal/service/user_service.go#L134-L140)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L52-L121)
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
- [internal/types/common.go](file://internal/types/common.go#L68-L79)
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
## 性能考虑
- 预签名URL有效期为15分钟建议客户端在有效期内完成上传避免重复生成
- 上传大小限制与扩展名校验在服务端进行,减少无效上传对存储的压力
- 对象键包含时间戳,便于按用户与时间维度管理与清理
- 存储层使用S3兼容SDK具备良好的并发与连接复用能力
## 故障排查指南
- 生成上传URL失败
- 检查file_name是否为空或扩展名不被允许
- 检查RustFS配置中的endpoint、use_ssl、buckets映射是否正确
- 检查对象存储连通性与权限
- 上传失败
- 确认客户端使用返回的post_url与form_data进行直传
- 确认上传未超过大小限制或超出有效期
- 更新头像URL失败
- 检查avatar_url是否为空
- 检查数据库连接与用户是否存在
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L52-L121)
## 结论
头像管理通过“服务端签发+客户端直传”的方式实现了高效、可控的头像上传流程。upload_service与storage包紧密协作确保上传策略的安全与可靠user_service负责最终的数据库更新。配合明确的请求参数、返回字段与900秒有效期整体方案简洁清晰、易于维护与扩展。
## 附录
### curl示例完整头像上传流程
- 步骤1生成预签名上传URL
- 请求
- 方法POST
- 地址:/api/v1/user/avatar/upload-url
- 请求体包含file_name
- 响应
- 返回post_url、form_data、avatar_url、expires_in
- 步骤2客户端直传到对象存储
- 使用post_url与form_data发起HTTP POST上传
- 上传成功后得到对象存储返回
- 步骤3通知后端更新数据库
- 请求
- 方法PUT
- 地址:/api/v1/user/avatar?avatar_url={最终访问URL}
- 响应
- 返回更新后的用户信息
说明
- expires_in为900秒15分钟在此期间内完成上传与更新
- 上传大小限制与扩展名限制由服务端在生成预签名URL时生效
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
- [internal/types/common.go](file://internal/types/common.go#L68-L79)

View File

@@ -1,384 +0,0 @@
# 用户API
<cite>
**本文引用的文件**
- [internal/handler/routes.go](file://internal/handler/routes.go)
- [internal/middleware/auth.go](file://internal/middleware/auth.go)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go)
- [internal/service/user_service.go](file://internal/service/user_service.go)
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
- [internal/service/verification_service.go](file://internal/service/verification_service.go)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
- [internal/types/common.go](file://internal/types/common.go)
- [internal/model/response.go](file://internal/model/response.go)
- [internal/model/user.go](file://internal/model/user.go)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go)
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
- [pkg/config/config.go](file://pkg/config/config.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能与安全考量](#性能与安全考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者与集成方系统化梳理用户管理API覆盖以下能力
- 获取与更新用户个人资料
- 更换邮箱(基于验证码)
- 头像上传通过预签名URL
- JWT认证中间件的工作机制
- 数据结构、请求/响应模式、错误处理与常见问题
## 项目结构
用户相关接口位于统一的路由组“/api/v1/user”该组下所有端点均受JWT认证保护。认证中间件负责从请求头中提取并校验Bearer Token并将用户标识写入上下文供业务层使用。
```mermaid
graph TB
Client["客户端"] --> Routes["路由注册<br/>/api/v1/user/*"]
Routes --> AuthMW["认证中间件<br/>AuthMiddleware()"]
AuthMW --> Handlers["用户处理器<br/>user_handler.go"]
Handlers --> Services["用户服务<br/>user_service.go"]
Services --> Repos["用户仓储<br/>user_repository.go"]
Services --> UploadSvc["上传服务<br/>upload_service.go"]
UploadSvc --> Storage["对象存储客户端<br/>minio.go"]
Services --> VeriSvc["验证码服务<br/>verification_service.go"]
Services --> JWT["JWT服务<br/>jwt.go"]
Services --> Types["类型定义<br/>types/common.go"]
Handlers --> Models["响应模型<br/>response.go"]
Handlers --> UserModel["用户模型<br/>model/user.go"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/middleware/auth.go](file://internal/middleware/auth.go#L12-L56)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L416)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L1-L121)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
## 核心组件
- 路由与认证
- 路由组“/api/v1/user”下的所有端点均使用认证中间件保护。
- 中间件从Authorization头解析Bearer Token调用JWT服务进行校验并将用户ID、用户名、角色写入上下文。
- 用户处理器
- 提供获取/更新用户资料、生成头像上传URL、更新头像、更换邮箱等接口。
- 服务层
- 用户服务封装业务逻辑(如修改密码、更换邮箱、更新头像),并调用仓储层持久化。
- 上传服务负责生成预签名URLPOST策略并构造最终可访问的文件URL。
- 验证码服务负责生成、发送与校验验证码(更换邮箱场景)。
- 仓储层
- 提供用户查询、更新、软删除等基础操作。
- 类型与模型
- 统一响应结构、用户信息结构、请求参数结构等。
- 用户模型包含基本字段及JSONB属性字段。
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/middleware/auth.go](file://internal/middleware/auth.go#L12-L56)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L416)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
## 架构总览
用户API采用“路由-中间件-处理器-服务-仓储-存储/外部服务”的分层架构。JWT中间件贯穿所有用户相关端点确保只有合法令牌才能访问。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由(/api/v1/user/*)"
participant M as "认证中间件"
participant H as "用户处理器"
participant S as "用户服务"
participant U as "上传服务"
participant V as "验证码服务"
participant D as "仓储"
participant T as "JWT服务"
participant O as "对象存储"
C->>R : "携带Authorization : Bearer <token>"
R->>M : "进入中间件"
M->>T : "校验token"
T-->>M : "Claims(用户ID/用户名/角色)"
M->>H : "放行,写入上下文"
alt 获取/更新资料
H->>S : "GetUserByID/UpdateUserInfo"
S->>D : "查询/更新"
D-->>S : "结果"
S-->>H : "用户信息"
H-->>C : "200 成功响应"
else 生成头像上传URL
H->>U : "GenerateAvatarUploadURL"
U->>O : "生成预签名POST策略"
O-->>U : "PostURL+FormData+FileURL"
U-->>H : "结果"
H-->>C : "200 成功响应"
else 更换邮箱
H->>V : "VerifyCode(验证码)"
V-->>H : "校验通过/失败"
H->>S : "ChangeUserEmail"
S->>D : "更新邮箱"
D-->>S : "结果"
S-->>H : "用户信息"
H-->>C : "200 成功响应"
end
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/middleware/auth.go](file://internal/middleware/auth.go#L12-L56)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L416)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L1-L121)
## 详细组件分析
### 认证中间件 AuthMiddleware
- 功能
- 从请求头Authorization中解析Bearer token
- 调用JWT服务校验token有效性
- 校验通过后将用户ID、用户名、角色写入上下文供后续处理器使用
- 异常
- 缺少Authorization头、格式不正确、token无效时返回401
- 适用范围
- 所有“/api/v1/user/*”端点均受此中间件保护
章节来源
- [internal/middleware/auth.go](file://internal/middleware/auth.go#L12-L56)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
### 用户资料:获取与更新
- 获取用户资料
- 方法GET /api/v1/user/profile
- 认证需要Bearer token
- 成功响应包含用户基本信息ID、用户名、邮箱、头像、积分、角色、状态、时间戳等
- 错误:未授权、用户不存在
- 更新用户资料
- 方法PUT /api/v1/user/profile
- 认证需要Bearer token
- 请求体支持更新头像URL若提供新密码必须同时提供旧密码
- 成功响应:返回更新后的用户信息
- 错误:参数错误、未授权、服务器错误、用户不存在
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L193)
- [internal/types/common.go](file://internal/types/common.go#L42-L47)
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
- [internal/model/user.go](file://internal/model/user.go#L1-L21)
### 头像上传流程预签名URL
- 生成上传URL
- 方法POST /api/v1/user/avatar/upload-url
- 认证需要Bearer token
- 请求体文件名file_name
- 成功响应包含PostURL、FormData、AvatarURL、过期秒数ExpiresIn
- 错误参数错误、未授权、生成URL失败
- 上传与确认
- 客户端使用返回的PostURL与FormData直传至对象存储
- 上传完成后调用PUT /api/v1/user/avatar携带avatar_url查询参数
- 服务端更新用户头像字段并返回最新用户信息
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "用户处理器"
participant U as "上传服务"
participant S as "对象存储客户端"
participant D as "仓储"
C->>H : "POST /api/v1/user/avatar/upload-url {file_name}"
H->>U : "GenerateAvatarUploadURL(userID, file_name)"
U->>S : "生成预签名POST策略"
S-->>U : "PostURL+FormData+FileURL"
U-->>H : "返回结果"
H-->>C : "200 {post_url, form_data, avatar_url, expires_in}"
C->>S : "使用PostURL+FormData直传"
S-->>C : "204/201"
C->>H : "PUT /api/v1/user/avatar?avatar_url=..."
H->>D : "UpdateUserAvatar"
D-->>H : "成功"
H-->>C : "200 用户信息"
```
图表来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L1-L121)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L126-L130)
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L195-L326)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L1-L121)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L126-L130)
### 更换邮箱(验证码)
- 流程
- 客户端先向验证码服务发送验证码(此处为通用流程,更换邮箱场景使用特定类型)
- 调用POST /api/v1/user/change-email携带新邮箱与验证码
- 服务端校验验证码,通过后更新用户邮箱并返回最新用户信息
- 请求体
- 新邮箱new_email
- 验证码verification_code
- 成功/错误
- 成功:返回用户信息
- 错误:参数错误、未授权、验证码错误、邮箱已被占用等
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [internal/service/user_service.go](file://internal/service/user_service.go#L186-L201)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L45-L57)
### 数据结构与请求/响应模式
- 用户信息结构UserInfo
- 字段id、username、email、avatar、points、role、status、last_login_at、created_at、updated_at
- 请求体
- 更新用户UpdateUserRequestavatar、old_password、new_password
- 生成头像上传URLGenerateAvatarUploadURLRequestfile_name
- 更换邮箱ChangeEmailRequestnew_email、verification_code
- 通用响应
- 成功code=200message=“操作成功”data为具体数据
- 失败code为对应HTTP语义码message为错误描述开发环境可带error字段
章节来源
- [internal/types/common.go](file://internal/types/common.go#L42-L47)
- [internal/types/common.go](file://internal/types/common.go#L62-L67)
- [internal/types/common.go](file://internal/types/common.go#L68-L80)
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
- [internal/model/user.go](file://internal/model/user.go#L1-L21)
## 依赖关系分析
- 路由到处理器
- /api/v1/user/profile GET/PUT -> GetUserProfile/UpdateUserProfile
- /api/v1/user/avatar/upload-url POST -> GenerateAvatarUploadURL
- /api/v1/user/avatar PUT -> UpdateAvatar
- /api/v1/user/change-email POST -> ChangeEmail
- 处理器到服务
- 用户处理器调用用户服务、上传服务、验证码服务
- 服务到仓储/存储
- 用户服务调用仓储更新用户信息
- 上传服务调用对象存储客户端生成预签名URL
- 中间件到JWT
- 认证中间件依赖JWT服务校验token并注入用户上下文
```mermaid
graph LR
Routes["routes.go"] --> Handler["user_handler.go"]
Handler --> UserService["user_service.go"]
Handler --> UploadService["upload_service.go"]
Handler --> VeriService["verification_service.go"]
UserService --> Repo["user_repository.go"]
UploadService --> Storage["minio.go"]
Handler --> Types["types/common.go"]
Handler --> Resp["response.go"]
Handler --> ModelUser["model/user.go"]
AuthMW["auth.go"] --> JWT["jwt.go"]
Handler --> AuthMW
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L416)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/middleware/auth.go](file://internal/middleware/auth.go#L12-L56)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L1-L121)
## 性能与安全考量
- 性能
- 上传采用预签名POST策略避免服务端中转降低带宽与延迟
- 对象存储客户端连接复用建议合理配置RustFS/Buckets与连接参数
- 安全
- 所有用户相关端点强制JWT认证
- 上传URL带过期时间防止长期有效链接泄露
- 验证码更换邮箱,避免暴力破解
- 密码修改需提供旧密码,防止越权修改
- 可靠性
- 上传URL生成失败、验证码校验失败、仓储更新失败均有明确错误返回
- 建议对高频接口增加限流与熔断策略(可在网关或中间件层实现)
[本节为通用指导,不直接分析具体文件]
## 故障排查指南
- 401 未授权
- 检查Authorization头是否为Bearer token格式
- 确认token未过期且签名正确
- 400 参数错误
- 检查请求体字段是否符合约束(如邮箱格式、验证码长度、文件名等)
- 404 用户不存在
- 确认用户ID有效且未被软删除
- 上传失败
- 检查生成的PostURL与FormData是否完整
- 确认对象存储端点、证书、桶名配置正确
- 验证码错误
- 检查Redis中验证码是否过期或被提前消费
- 确认发送频率限制未触发
章节来源
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L416)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L1-L121)
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
## 结论
本文档系统梳理了用户管理API的路由、认证、数据结构与流程明确了头像上传与邮箱更换的关键步骤与错误处理策略。建议在生产环境中结合限流、监控与日志体系持续优化用户体验与系统稳定性。
[本节为总结,不直接分析具体文件]
## 附录
### 接口一览与示例
- 获取用户资料
- 方法GET /api/v1/user/profile
- 请求Authorization: Bearer <token>
- 成功响应包含UserInfo
- 更新用户资料
- 方法PUT /api/v1/user/profile
- 请求体UpdateUserRequestavatar、old_password、new_password
- 成功响应包含更新后的UserInfo
- 生成头像上传URL
- 方法POST /api/v1/user/avatar/upload-url
- 请求体GenerateAvatarUploadURLRequestfile_name
- 成功响应包含post_url、form_data、avatar_url、expires_in
- 上传头像并确认
- 方法PUT /api/v1/user/avatar?avatar_url=...
- 成功响应包含UserInfo
- 更换邮箱
- 方法POST /api/v1/user/change-email
- 请求体ChangeEmailRequestnew_email、verification_code
- 成功响应包含UserInfo
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L416)
- [internal/types/common.go](file://internal/types/common.go#L42-L47)
- [internal/types/common.go](file://internal/types/common.go#L62-L67)
- [internal/types/common.go](file://internal/types/common.go#L68-L80)

View File

@@ -1,336 +0,0 @@
# 邮箱变更
<cite>
**本文引用的文件**
- [user_handler.go](file://internal/handler/user_handler.go)
- [routes.go](file://internal/handler/routes.go)
- [auth.go](file://internal/middleware/auth.go)
- [verification_service.go](file://internal/service/verification_service.go)
- [user_service.go](file://internal/service/user_service.go)
- [user_repository.go](file://internal/repository/user_repository.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [redis.go](file://pkg/redis/redis.go)
- [email.go](file://pkg/email/email.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能与可靠性](#性能与可靠性)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本文档围绕“POST /api/v1/user/change-email”端点系统化阐述邮箱变更的完整流程与技术实现细节。该接口要求用户提供新邮箱地址与验证码后端通过验证服务核验Redis中存储的验证码随后由用户服务层执行数据库更新确保邮箱唯一性与安全性。文档还解释了请求体结构、验证码生成与存储机制、错误处理策略并给出成功与失败场景的响应示例强调该操作需要有效的JWT认证。
## 项目结构
- 路由与鉴权
- 路由在 v1 组下用户相关接口均受JWT中间件保护。
- 邮箱变更接口位于 /api/v1/user/change-email采用POST方法。
- 控制器层
- 用户控制器负责接收请求、参数校验、调用服务层、组装响应。
- 服务层
- 验证服务生成、发送、校验验证码验证码以特定键规则存入Redis。
- 用户服务:执行邮箱唯一性检查与数据库更新。
- 数据访问层
- 用户仓库封装GORM操作提供按邮箱查询与字段更新能力。
- 基础设施
- Redis客户端提供键值存取、过期控制、删除等操作。
- 邮件服务:根据类型发送不同主题的验证码邮件。
```mermaid
graph TB
Client["客户端"] --> Routes["路由: /api/v1/user/change-email"]
Routes --> AuthMW["JWT中间件"]
AuthMW --> Handler["用户处理器: ChangeEmail"]
Handler --> VerifySvc["验证服务: VerifyCode"]
Handler --> UserSvc["用户服务: ChangeUserEmail"]
VerifySvc --> Redis["Redis: 验证码存储"]
UserSvc --> Repo["用户仓库: UpdateUserFields"]
Repo --> DB["数据库: user 表"]
VerifySvc --> Email["邮件服务: SendChangeEmail"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L1-L120)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L47-L55)
章节来源
- [routes.go](file://internal/handler/routes.go#L1-L120)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
## 核心组件
- 请求体结构
- ChangeEmailRequest.new_email新邮箱地址必填且符合邮箱格式。
- ChangeEmailRequest.verification_code验证码必填且长度为6。
- 鉴权要求
- 所有用户相关接口均需携带有效的Bearer JWT。
- 验证码机制
- 验证码类型常量包含“change_email”用于区分不同用途。
- 验证码长度为6有效期10分钟发送频率限制1分钟。
- 验证通过后Redis中的验证码键会被删除。
- 邮箱唯一性校验
- 在更新邮箱前,查询数据库确认新邮箱未被其他用户占用;若被占用则拒绝。
- 错误处理
- 参数错误、未授权、验证码错误、邮箱已被使用、服务器内部错误等均有明确响应码与消息。
章节来源
- [common.go](file://internal/types/common.go#L62-L66)
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [response.go](file://internal/model/response.go#L27-L53)
## 架构总览
POST /api/v1/user/change-email 的端到端流程如下:
1. 客户端携带JWT向 /api/v1/user/change-email 发起POST请求请求体包含 new_email 与 verification_code。
2. 路由匹配到用户组JWT中间件校验通过后进入用户处理器。
3. 处理器解析请求体并调用验证服务验证Redis中对应类型的验证码是否匹配。
4. 验证通过后,处理器调用用户服务更新数据库中的邮箱字段。
5. 更新完成后,重新查询用户信息并返回成功响应。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由"
participant M as "JWT中间件"
participant H as "用户处理器"
participant VS as "验证服务"
participant RS as "Redis"
participant US as "用户服务"
participant UR as "用户仓库"
participant DB as "数据库"
C->>R : "POST /api/v1/user/change-email"
R->>M : "鉴权"
M-->>R : "通过"
R->>H : "进入ChangeEmail"
H->>VS : "VerifyCode(email, code, type=change_email)"
VS->>RS : "Get(verification : code : change_email : new_email)"
RS-->>VS : "验证码"
VS-->>H : "验证通过"
H->>US : "ChangeUserEmail(userID, new_email)"
US->>UR : "FindUserByEmail(new_email)"
UR-->>US : "查询结果"
US->>UR : "UpdateUserFields(userID, {email : new_email})"
UR->>DB : "更新"
DB-->>UR : "成功"
UR-->>US : "成功"
US-->>H : "成功"
H-->>C : "返回用户信息"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L27-L41)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
## 详细组件分析
### 控制器ChangeEmail 端点
- 功能职责
- 从上下文提取用户IDJWT中间件注入
- 解析请求体为 ChangeEmailRequest。
- 调用验证服务核验验证码(类型为 change_email
- 调用用户服务更新邮箱。
- 重新查询用户信息并返回成功响应。
- 错误处理
- 缺少Authorization头或无效token返回401。
- 请求体绑定失败返回400。
- 验证码错误或过期返回400。
- 邮箱已被占用返回400。
- 用户不存在返回404。
- 其他异常返回500。
```mermaid
flowchart TD
Start(["进入ChangeEmail"]) --> GetCtx["获取user_id"]
GetCtx --> CheckAuth{"user_id存在?"}
CheckAuth -- 否 --> Resp401["返回401未授权"]
CheckAuth -- 是 --> BindReq["绑定ChangeEmailRequest"]
BindReq --> BindOK{"绑定成功?"}
BindOK -- 否 --> Resp400["返回400参数错误"]
BindOK -- 是 --> Verify["调用VerifyCode(type=change_email)"]
Verify --> VerifyOK{"验证通过?"}
VerifyOK -- 否 --> Resp400V["返回400验证码错误/过期"]
VerifyOK -- 是 --> Update["调用ChangeUserEmail"]
Update --> UpdateOK{"更新成功?"}
UpdateOK -- 否 --> Resp400E["返回400邮箱已被使用/其他错误"]
UpdateOK -- 是 --> Reload["重新查询用户信息"]
Reload --> Resp200["返回200成功"]
```
图表来源
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [response.go](file://internal/model/response.go#L27-L53)
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [response.go](file://internal/model/response.go#L27-L53)
### 验证服务:验证码生成、发送与校验
- 生成与存储
- 生成6位数字验证码。
- 以键模式 verification:code:{type}:{email} 写入Redis有效期10分钟。
- 发送频率限制键 verification:rate_limit:{type}:{email} 设置1分钟。
- 校验逻辑
- 读取Redis中的验证码并与请求一致进行比对。
- 校验通过后删除该验证码键。
- 邮件发送
- 根据类型选择不同主题与正文,发送更换邮箱验证码邮件。
```mermaid
flowchart TD
Gen["生成6位验证码"] --> Store["写入Redis: code键(10分钟)"]
Store --> Rate["设置rate_limit键(1分钟)"]
Rate --> Mail["发送邮件(ChangeEmail)"]
Mail --> Verify["VerifyCode: 读取code键"]
Verify --> Match{"是否匹配?"}
Match -- 否 --> Err["返回错误(验证码错误/过期)"]
Match -- 是 --> Del["删除code键"]
Del --> Ok["返回成功"]
```
图表来源
- [verification_service.go](file://internal/service/verification_service.go#L26-L77)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [email.go](file://pkg/email/email.go#L47-L55)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
章节来源
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [verification_service.go](file://internal/service/verification_service.go#L26-L77)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [email.go](file://pkg/email/email.go#L47-L55)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
### 用户服务:邮箱唯一性与更新
- 唯一性检查
- 查询新邮箱是否已被其他用户占用若存在且ID不同则报错。
- 更新逻辑
- 使用UpdateUserFields更新邮箱字段。
```mermaid
flowchart TD
Check["FindUserByEmail(new_email)"] --> Found{"是否找到用户?"}
Found -- 是且ID!=userID --> Err["返回错误: 邮箱已被使用"]
Found -- 否或ID==userID --> Update["UpdateUserFields(userID, {email: new_email})"]
Update --> Done["返回成功"]
```
图表来源
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [user_repository.go](file://internal/repository/user_repository.go#L45-L57)
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
章节来源
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [user_repository.go](file://internal/repository/user_repository.go#L45-L57)
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
### 数据模型与响应
- 请求体
- ChangeEmailRequest.new_email新邮箱地址必填且符合邮箱格式。
- ChangeEmailRequest.verification_code验证码必填且长度为6。
- 成功响应
- 返回通用响应结构,包含用户信息(含更新后的邮箱)。
- 错误响应
- 400请求参数错误、验证码错误/过期、邮箱已被使用。
- 401未授权。
- 404用户不存在。
- 500服务器内部错误。
章节来源
- [common.go](file://internal/types/common.go#L62-L66)
- [response.go](file://internal/model/response.go#L27-L53)
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
## 依赖关系分析
- 控制器依赖
- 路由与JWT中间件保证接口受保护。
- 验证服务:负责验证码核验。
- 用户服务:负责邮箱更新与唯一性检查。
- 服务层依赖
- Redis客户端验证码存取与删除。
- 邮件服务:发送验证码邮件。
- 用户仓库:数据库读写。
- 数据库约束
- 用户表的邮箱字段具备唯一索引,配合服务层检查可避免并发冲突。
```mermaid
graph LR
Handler["用户处理器"] --> VerifySvc["验证服务"]
Handler --> UserSvc["用户服务"]
VerifySvc --> Redis["Redis客户端"]
VerifySvc --> Email["邮件服务"]
UserSvc --> Repo["用户仓库"]
Repo --> DB["数据库"]
```
图表来源
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L47-L55)
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L47-L55)
## 性能与可靠性
- 验证码缓存
- Redis键过期时间10分钟避免长期占用内存rate_limit键1分钟限制发送频率降低滥用风险。
- 并发一致性
- 服务层在更新前进行邮箱唯一性检查,结合数据库唯一索引,有效防止并发写入导致的重复。
- 日志与可观测性
- 处理器与服务层在关键路径记录日志,便于定位问题。
- 错误传播
- 明确的错误码与消息,便于前端统一处理。
[本节为通用指导,不直接分析具体文件]
## 故障排查指南
- 401 未授权
- 检查请求头 Authorization 是否为 Bearer 令牌,且令牌有效。
- 400 请求参数错误
- 确认请求体包含 new_email 与 verification_code且格式正确。
- 400 验证码错误/过期
- 检查Redis中 verification:code:change_email:{email} 是否存在且未过期;确认邮件是否送达。
- 400 邮箱已被使用
- 确认新邮箱未被其他用户占用;若被占用请更换邮箱。
- 404 用户不存在
- 检查用户是否被软删除或账户状态异常。
- 500 服务器内部错误
- 查看服务日志关注数据库更新与Redis存取异常。
章节来源
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L186-L201)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [response.go](file://internal/model/response.go#L27-L53)
## 结论
POST /api/v1/user/change-email 通过严格的JWT鉴权、Redis验证码校验与数据库唯一性检查实现了安全可靠的邮箱变更流程。请求体简洁明确错误处理清晰适合在生产环境中稳定运行。建议在部署时确保Redis与邮件服务可用并合理配置验证码有效期与发送频率限制以兼顾用户体验与安全。

View File

@@ -1,261 +0,0 @@
# 系统API
<cite>
**本文引用的文件**
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
- [internal/handler/routes.go](file://internal/handler/routes.go)
- [internal/model/system_config.go](file://internal/model/system_config.go)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
- [README.md](file://README.md)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件聚焦于系统相关的两个关键API健康检查与系统配置获取。前者用于服务可用性监控后者用于获取公开的全局配置项帮助前端与运维侧快速了解站点状态与功能开关。文档将结合路由注册、处理器实现、模型与仓库层给出端点定义、响应结构、调用流程与最佳实践并说明Swagger文档的访问方式。
## 项目结构
系统API位于内部处理层handler通过路由注册挂载至Gin引擎系统配置模型与仓库层负责持久化与查询Swagger文档通过中间件暴露静态页面与健康检查端点。
```mermaid
graph TB
subgraph "路由与处理器"
Routes["routes.go<br/>注册 /api/v1/system/config"]
Swagger["swagger.go<br/>注册 /health 与 /swagger/*any"]
Health["HealthCheck()<br/>返回 {status,message}"]
SysCfg["GetSystemConfig()<br/>返回公开配置"]
end
subgraph "模型与仓库"
ModelSC["SystemConfig 模型<br/>公开字段: site_name, registration_enabled, ..."]
RepoSC["system_config_repository<br/>GetPublicSystemConfigs()"]
end
Routes --> SysCfg
Swagger --> Health
SysCfg --> RepoSC
RepoSC --> ModelSC
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
## 核心组件
- 健康检查端点
- 路径:/health
- 方法GET
- 作用返回服务运行状态便于Kubernetes、负载均衡器或监控系统进行存活探针与就绪探针
- 响应:包含状态与消息字段的对象
- 系统配置端点
- 路径:/api/v1/system/config
- 方法GET
- 作用:返回公开的系统配置集合,供前端渲染站点标题、描述、注册开关、用户限制等
- 当前实现:返回硬编码的公开配置;后续可替换为从数据库读取
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
## 架构总览
系统API的调用链路如下客户端请求 -> Gin路由 -> 处理器函数 -> 仓库层(当前为内存返回,未来可改为数据库查询)-> 返回响应。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "Gin路由"
participant H as "处理器"
participant M as "模型/仓库(当前为内存)"
participant S as "Swagger/健康检查"
C->>R : GET /health
R->>S : HealthCheck()
S-->>C : {status,message}
C->>R : GET /api/v1/system/config
R->>H : GetSystemConfig()
H->>M : 当前返回硬编码公开配置
M-->>H : 公开配置对象
H-->>C : 成功响应
```
图表来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
## 详细组件分析
### 健康检查端点 /health
- 注册位置Swagger设置函数中注册了 /health 路由
- 处理逻辑:返回固定的成功状态与消息
- 响应结构:包含状态与消息字段的对象
- 用途作为Kubernetes探针、负载均衡器健康检查、CI流水线可用性检测
```mermaid
flowchart TD
Start(["请求进入 /health"]) --> Check["调用 HealthCheck()"]
Check --> BuildResp["构建响应对象<br/>包含 status 与 message"]
BuildResp --> Return["返回 200 OK"]
```
图表来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
### 系统配置端点 /api/v1/system/config
- 注册位置v1路由组下的 /system/config
- 处理逻辑:当前实现返回硬编码的公开配置;注释提示后续应从数据库读取
- 响应结构:当前返回包含站点名称、描述、注册开关、每用户材质与档案上限等字段的对象
- 模型与仓库:
- 模型定义了公开配置的结构(站点名称、描述、注册开关、维护模式、公告等)
- 仓库提供了获取公开配置的方法(按 is_public 过滤)
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "Gin路由"
participant H as "GetSystemConfig()"
participant Repo as "GetPublicSystemConfigs()"
participant DB as "数据库"
C->>R : GET /api/v1/system/config
R->>H : 调用处理器
alt 当前实现为内存返回
H-->>C : 返回硬编码公开配置
else 后续实现为数据库读取
H->>Repo : 查询 is_public=true 的配置
Repo->>DB : 执行查询
DB-->>Repo : 返回配置列表
Repo-->>H : 配置对象
H-->>C : 返回公开配置
end
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
### Swagger 文档访问
- 路由:/swagger/*any
- 作用提供交互式API文档便于开发者与测试人员查阅接口定义、参数与示例
- 项目说明README中明确给出了Swagger文档地址与健康检查地址
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
- [README.md](file://README.md#L149-L153)
## 依赖分析
- 路由注册依赖:
- Swagger设置函数负责注册 /health 与 /swagger/*any
- v1路由组负责注册 /api/v1/system/config
- 处理器依赖:
- GetSystemConfig 当前为内存返回;若改为数据库读取,则依赖仓库层的公开配置查询
- 模型与仓库:
- SystemConfig 模型定义公开配置字段
- 仓库层提供按 is_public 过滤的查询方法
```mermaid
graph LR
Swagger["swagger.go"] --> Routes["routes.go"]
Routes --> HandlerSys["GetSystemConfig()"]
HandlerSys --> Repo["system_config_repository.go"]
Repo --> Model["system_config.go"]
```
图表来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
## 性能考虑
- 健康检查:纯内存返回,延迟极低,适合高频探针
- 系统配置:
- 当前为内存返回,避免数据库开销
- 若切换为数据库查询,建议:
- 对公开配置建立索引(如按 is_public 过滤)
- 引入缓存层如Redis短期缓存公开配置减少数据库压力
- 控制响应大小,仅返回必要字段
- Swagger文档静态文件托管对性能影响可忽略
## 故障排查指南
- 健康检查失败
- 检查服务进程是否正常运行
- 确认 /health 路由已注册Swagger设置函数
- 使用浏览器或curl访问 /health观察返回状态与内容
- 系统配置端点异常
- 当前实现为内存返回,若返回异常,检查处理器逻辑
- 若切换为数据库查询,检查数据库连接、表是否存在、字段是否正确
- Swagger文档无法访问
- 确认 /swagger/*any 路由已注册
- 访问 /swagger/index.html 查看文档
- 若文档为空检查是否已生成Swagger文档
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [README.md](file://README.md#L149-L153)
## 结论
- /health 提供轻量级服务可用性检查,适合作为探针与监控指标
- /api/v1/system/config 当前返回硬编码公开配置,满足前端基础展示需求;后续可无缝替换为数据库读取,配合缓存与索引提升性能
- Swagger文档提供便捷的接口浏览与调试入口有助于开发与运维协作
## 附录
### API定义与响应示例
- 健康检查
- 方法GET
- 路径:/health
- 响应示例(结构示意):
- status: 字符串,表示服务状态
- message: 字符串,简要描述
- 用途:存活/就绪探针、CI流水线可用性检测
- 系统配置
- 方法GET
- 路径:/api/v1/system/config
- 响应示例(结构示意):
- site_name: 字符串,站点名称
- site_description: 字符串,站点描述
- registration_enabled: 布尔值,是否允许注册
- max_textures_per_user: 整数,每用户最大材质数
- max_profiles_per_user: 整数,每用户最大档案数
- 用途:前端渲染站点标题、描述、注册开关、用户限制等
- Swagger文档
- 访问路径:/swagger/index.html
- 用途查看与调试所有API接口
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
- [README.md](file://README.md#L149-L153)

View File

@@ -1,330 +0,0 @@
# 发送验证码
<cite>
**本文引用的文件**
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [routes.go](file://internal/handler/routes.go)
- [verification_service.go](file://internal/service/verification_service.go)
- [common.go](file://internal/types/common.go)
- [email.go](file://pkg/email/email.go)
- [manager.go](file://pkg/email/manager.go)
- [redis.go](file://pkg/redis/redis.go)
- [manager.go](file://pkg/redis/manager.go)
- [config.go](file://pkg/config/config.go)
- [main.go](file://cmd/server/main.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向“发送验证码”API围绕 /api/v1/auth/send-code 端点进行完整说明。内容包括:
- 接口概述与HTTP规范
- 请求体结构与字段约束email、type
- 响应格式与错误码
- 验证码生成、存储与过期策略Redis
- 邮件服务集成pkg/email
- 不同验证码类型(注册、重置密码、更换邮箱)的处理逻辑
- 实际请求与响应示例
- 安全注意事项(频率限制、邮箱格式)
## 项目结构
/api/v1/auth/send-code 属于认证模块,位于 Gin 路由分组 /api/v1/auth 下,由处理器负责接收请求、绑定参数、调用服务层发送验证码,并通过 Redis 与邮件服务完成验证码的持久化与发送。
```mermaid
graph TB
Client["客户端"] --> Router["Gin 路由<br/>/api/v1/auth/send-code"]
Router --> Handler["处理器<br/>SendVerificationCode"]
Handler --> Service["服务层<br/>SendVerificationCode/VerifyCode"]
Service --> Redis["Redis 客户端<br/>存储验证码/频率限制"]
Service --> Email["邮件服务<br/>发送验证码邮件"]
Email --> SMTP["SMTP 服务器"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L16-L26)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L29-L105)
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L26)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
## 核心组件
- 路由与处理器
- 路由注册:/api/v1/auth/send-code 绑定到 SendVerificationCode 处理器。
- 处理器职责:参数绑定、调用服务层发送验证码、记录日志、返回统一响应。
- 服务层
- 生成6位数字验证码按 type 与 email 组合键存储至 Redis设置过期时间设置发送频率限制调用邮件服务发送对应类型的邮件。
- Redis
- 提供 Set/Get/Del/Exists 等基础操作,封装过期时间控制与错误处理。
- 邮件服务
- 基于 SMTP 的邮件发送,支持 465隐式 TLS与 587显式 TLS端口根据 type 选择不同主题与正文模板。
- 类型定义
- SendVerificationCodeRequest 包含 email 与 type 字段type 限定为 register/reset_password/change_email。
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L26)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L29-L105)
- [common.go](file://internal/types/common.go#L49-L54)
## 架构总览
发送验证码的端到端流程如下:
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "处理器<br/>SendVerificationCode"
participant S as "服务层<br/>SendVerificationCode/VerifyCode"
participant R as "Redis 客户端"
participant E as "邮件服务<br/>SendVerificationCode"
participant M as "SMTP 服务器"
C->>H : POST /api/v1/auth/send-code<br/>{email, type}
H->>H : 绑定请求体并校验
H->>S : SendVerificationCode(ctx, redis, email, type)
S->>R : Exists(rate_limit_key)
alt 已存在
S-->>H : 返回“发送过于频繁”
H-->>C : 400 错误
else 不存在
S->>S : 生成6位数字验证码
S->>R : Set(code_key, code, 10分钟)
S->>R : Set(rate_limit_key, 1, 1分钟)
S->>E : 根据type发送邮件
E->>M : 发送邮件
M-->>E : 成功/失败
alt 邮件发送失败
S->>R : Del(code_key)
S-->>H : 返回“发送邮件失败”
H-->>C : 400 错误
else 成功
S-->>H : 返回成功
H-->>C : 200 成功
end
end
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L29-L105)
## 详细组件分析
### HTTP 端点定义
- 方法POST
- 路径:/api/v1/auth/send-code
- 功能:根据邮箱与类型发送验证码邮件,并在 Redis 中存储验证码及频率限制。
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L26)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
### 请求体结构
- 字段
- email: 必填,需符合邮箱格式
- type: 必填,枚举值为 register/reset_password/change_email
- 参数绑定与校验
- 使用 Gin 的绑定与验证机制,确保 email 符合邮箱格式type 在允许范围内。
章节来源
- [common.go](file://internal/types/common.go#L49-L54)
### 响应格式
- 成功
- 状态码200
- 结构:统一响应体,包含 message 字段提示“验证码已发送,请查收邮件”
- 失败
- 状态码400
- 结构:统一错误响应体,包含错误码与错误信息
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
### 错误码与错误场景
- 400 参数错误
- 请求体绑定失败或参数校验失败
- 400 发送过于频繁
- Redis 中存在频率限制键1分钟内
- 400 邮件发送失败
- 邮件服务未启用或 SMTP 发送异常
- 400 验证码已过期或不存在
- 验证码校验时 Redis 中无对应键
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
### 验证码生成与存储Redis
- 生成规则
- 6位数字验证码
- 存储键
- 验证码键verification:code:{type}:{email}
- 过期时间10分钟
- 频率限制
- 频率限制键verification:rate_limit:{type}:{email}
- 过期时间1分钟
- Redis 操作
- Set/SetEx写入验证码与频率限制
- Get读取验证码用于校验
- Del验证成功后删除验证码键
- Exists检查是否处于发送冷却
章节来源
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
### 邮件服务集成pkg/email
- 初始化
- 通过全局初始化函数完成一次性初始化,随后通过 MustGetService 获取实例
- 发送逻辑
- 根据 type 选择不同主题与正文模板
- 支持 465隐式 TLS与 587显式 TLS两种端口模式
- 当邮件服务未启用时,返回“邮件服务未启用”的错误
- 主题与正文
- 注册:邮箱验证
- 重置密码:重置密码
- 更换邮箱:更换邮箱验证
- 默认:通用验证码
章节来源
- [email.go](file://pkg/email/email.go#L29-L105)
- [manager.go](file://pkg/email/manager.go#L1-L43)
- [main.go](file://cmd/server/main.go#L71-L74)
### 不同验证码类型的处理逻辑
- 注册register
- 邮件主题:邮箱验证
- 通常配合注册接口使用,注册时需提供验证码
- 重置密码reset_password
- 邮件主题:重置密码
- 与 /api/v1/auth/reset-password 配合使用
- 更换邮箱change_email
- 邮件主题:更换邮箱验证
- 与用户更换邮箱流程配合使用
- 默认(其他)
- 通用验证码主题
章节来源
- [verification_service.go](file://internal/service/verification_service.go#L106-L118)
- [email.go](file://pkg/email/email.go#L107-L139)
### 安全考虑
- 邮箱格式验证
- 请求体中 email 字段使用邮箱格式校验
- 发送频率限制
- Redis 中按 type+email 维度设置1分钟冷却避免刷屏
- 验证码有效期
- Redis 中验证码设置10分钟过期过期即失效
- 重复使用防护
- 验证码校验成功后立即删除键,防止二次使用
- 邮件服务开关
- 若未启用邮件服务,发送验证码会直接失败,避免泄露敏感信息
章节来源
- [common.go](file://internal/types/common.go#L49-L54)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L29-L40)
## 依赖关系分析
- 处理器依赖服务层
- 服务层依赖 Redis 与邮件服务
- 邮件服务依赖配置与日志
- Redis 客户端依赖配置与日志
- 全局初始化顺序:配置 -> 日志 -> 数据库 -> JWT -> Redis -> 对象存储 -> 邮件服务
```mermaid
graph LR
H["处理器<br/>auth_handler.go"] --> S["服务层<br/>verification_service.go"]
S --> R["Redis 客户端<br/>redis.go"]
S --> E["邮件服务<br/>email.go"]
E --> Cfg["配置<br/>config.go"]
E --> Lg["日志"]
R --> Cfg
R --> Lg
Main["服务启动<br/>main.go"] --> E
Main --> R
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [email.go](file://pkg/email/email.go#L29-L105)
- [config.go](file://pkg/config/config.go#L49-L107)
- [main.go](file://cmd/server/main.go#L27-L74)
章节来源
- [main.go](file://cmd/server/main.go#L27-L74)
## 性能考量
- Redis 操作均为 O(1)Set/Get/Del/Exists 均为常数时间复杂度
- 验证码长度固定为6位生成与比较成本低
- 邮件发送为外部依赖受网络与SMTP服务器性能影响
- 建议
- 合理设置 Redis 连接池大小PoolSize
- 控制邮件发送并发避免瞬时高峰导致SMTP限流
- 对高频请求开启更严格的频率限制(如增加冷却时间)
[本节为通用性能建议,不直接分析具体文件]
## 故障排查指南
- 400 参数错误
- 检查请求体是否包含 email 与 type且 type 是否在允许范围内
- 400 发送过于频繁
- 等待1分钟冷却时间或检查 Redis 中 rate_limit 键是否仍存在
- 400 邮件发送失败
- 确认邮件服务已启用EMAIL_ENABLED=true
- 检查 SMTP 配置(主机、端口、用户名、密码、发件人名称)
- 查看日志中 SMTP 发送错误信息
- 400 验证码已过期或不存在
- 检查 Redis 中 code 键是否存在与过期时间
- 确认验证码是否已被验证成功后删除
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [email.go](file://pkg/email/email.go#L29-L105)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
## 结论
/api/v1/auth/send-code 端点通过 Gin 处理器接收请求调用服务层完成验证码生成、Redis 存储与频率限制、邮件发送等流程。其设计遵循最小暴露面原则:严格参数校验、冷却时间、过期时间与删除机制共同保障安全性与可用性。结合 pkg/email 与 pkg/redis 的稳定实现,整体具备良好的扩展性与可维护性。
[本节为总结性内容,不直接分析具体文件]
## 附录
### 请求与响应示例
- 请求示例
- 方法POST
- 路径:/api/v1/auth/send-code
- 请求体:
- email: user@example.com
- type: register 或 reset_password 或 change_email
- 成功响应示例
- 状态码200
- 响应体:包含 message 字段,提示“验证码已发送,请查收邮件”
- 失败响应示例
- 状态码400
- 响应体:包含错误码与错误信息,如“发送过于频繁,请稍后再试”或“发送邮件失败”
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [common.go](file://internal/types/common.go#L49-L54)

View File

@@ -1,352 +0,0 @@
# 用户注册
<cite>
**本文引用的文件**
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [routes.go](file://internal/handler/routes.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [user_service.go](file://internal/service/user_service.go)
- [verification_service.go](file://internal/service/verification_service.go)
- [password.go](file://pkg/auth/password.go)
- [user.go](file://internal/model/user.go)
- [auth_handler_test.go](file://internal/handler/auth_handler_test.go)
- [common_test.go](file://internal/types/common_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能与扩展性](#性能与扩展性)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向“用户注册”API围绕 /api/v1/auth/register 端点进行完整说明。内容覆盖:
- HTTP 方法与路由
- 请求体字段与校验规则
- 响应格式与错误码
- 验证码机制与防暴力注册策略
- 密码加密存储流程
- 安全建议(密码强度、邮箱格式、防暴力注册)
## 项目结构
该功能由 Handler 层接收请求、Service 层执行业务逻辑、Model 层承载数据模型,并通过 JWT 生成登录令牌;验证码由 Redis 缓存并在邮件服务中发送。
```mermaid
graph TB
Client["客户端"] --> Routes["路由: /api/v1/auth/register"]
Routes --> Handler["处理器: Register"]
Handler --> VerifyCode["验证码校验: VerifyCode"]
Handler --> UserService["服务: RegisterUser"]
UserService --> Password["密码加密: HashPassword"]
UserService --> ModelUser["用户模型: User"]
UserService --> JWT["JWT 服务: GenerateToken"]
Handler --> Response["统一响应: Response/ErrorResponse"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L16-L26)
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L12-L68)
- [password.go](file://pkg/auth/password.go#L7-L21)
- [user.go](file://internal/model/user.go#L7-L21)
- [response.go](file://internal/model/response.go#L20-L73)
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L26)
## 核心组件
- 路由与入口
- 路由位于 v1 组下的 /auth/register无需 JWT 即可访问。
- 请求体与校验
- 请求体字段username、email、password、avatar、verification_code。
- 校验规则:必填、长度/格式约束、验证码长度固定为6。
- 业务处理
- 验证码校验通过后,检查用户名/邮箱唯一性,加密密码,创建用户并生成 JWT。
- 响应与错误
- 成功返回包含 token 与用户信息的结构化响应;常见错误包括参数错误、验证码错误、用户名/邮箱已存在等。
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
- [common.go](file://internal/types/common.go#L33-L40)
- [response.go](file://internal/model/response.go#L20-L73)
- [user_service.go](file://internal/service/user_service.go#L12-L68)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
## 架构总览
注册流程的端到端调用序列如下:
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由"
participant H as "处理器 : Register"
participant VS as "验证码服务 : VerifyCode"
participant US as "用户服务 : RegisterUser"
participant PS as "密码服务 : HashPassword"
participant M as "用户模型 : User"
participant JS as "JWT服务 : GenerateToken"
participant RESP as "响应"
C->>R : POST /api/v1/auth/register
R->>H : 转发请求
H->>H : 解析并绑定请求体
H->>VS : 校验邮箱验证码
VS-->>H : 验证通过/失败
alt 验证失败
H-->>RESP : 返回400错误
else 验证通过
H->>US : 调用注册流程
US->>US : 检查用户名/邮箱唯一性
US->>PS : 加密密码
PS-->>US : 密文
US->>M : 创建用户记录
US->>JS : 生成JWT
JS-->>US : token
US-->>H : 返回用户与token
H-->>RESP : 返回200成功响应
end
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L12-L68)
- [password.go](file://pkg/auth/password.go#L7-L21)
- [user.go](file://internal/model/user.go#L7-L21)
- [response.go](file://internal/model/response.go#L20-L73)
## 详细组件分析
### 接口定义与请求体
- 端点POST /api/v1/auth/register
- 请求体字段
- username必填长度3~50
- email必填邮箱格式
- password必填长度6~128
- avatar可选URL格式
- verification_code必填长度6
- 响应
- 成功:返回包含 token 与用户信息的结构化响应
- 失败:返回统一错误响应,包含业务码与消息
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
- [common.go](file://internal/types/common.go#L33-L40)
- [response.go](file://internal/model/response.go#L20-L73)
### 验证码机制与防暴力注册
- 验证码生成与存储
- 生成6位数字验证码有效期10分钟发送频率限制1分钟。
- 存储于 Redis键命名包含类型与邮箱。
- 验证流程
- 校验验证码是否存在且一致,一致即删除该验证码。
- 防暴力注册
- 发送频率限制键存在即拒绝重复发送,降低刷验证码风险。
- 注册接口对用户名/邮箱唯一性检查,避免重复注册。
```mermaid
flowchart TD
Start(["开始"]) --> Gen["生成6位验证码"]
Gen --> Store["写入Redis: 验证码键(10分钟)"]
Store --> RateLimit["写入Redis: 发送频率限制键(1分钟)"]
RateLimit --> Email["发送邮件"]
Email --> Verify["校验验证码"]
Verify --> Match{"是否匹配?"}
Match --> |否| Expired["返回错误: 验证码错误/过期"]
Match --> |是| Del["删除验证码键"]
Del --> Done(["结束"])
```
图表来源
- [verification_service.go](file://internal/service/verification_service.go#L26-L98)
章节来源
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [verification_service.go](file://internal/service/verification_service.go#L40-L77)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
### 密码加密与存储
- 加密方式bcrypt默认成本
- 存储:用户密码以密文形式保存,不返回明文
- 登录校验:使用 bcrypt 对比哈希
```mermaid
flowchart TD
In(["输入明文密码"]) --> Hash["bcrypt加密"]
Hash --> Out(["返回密文"])
Out --> Store["持久化存储"]
Store --> Login["登录时对比哈希"]
Login --> Ok["匹配成功"]
Login --> Fail["匹配失败"]
```
图表来源
- [password.go](file://pkg/auth/password.go#L7-L21)
- [user_service.go](file://internal/service/user_service.go#L32-L36)
- [user.go](file://internal/model/user.go#L7-L21)
章节来源
- [password.go](file://pkg/auth/password.go#L7-L21)
- [user_service.go](file://internal/service/user_service.go#L32-L36)
- [user.go](file://internal/model/user.go#L7-L21)
### 用户模型与唯一性约束
- 用户模型包含id、username、password、email、avatar、points、role、status、属性、登录时间、创建/更新时间等
- 唯一性约束username 与 email 在数据库层面唯一索引
- 注册时:若用户名或邮箱已存在,返回错误
```mermaid
erDiagram
USER {
bigint id PK
varchar username UK
varchar password
varchar email UK
varchar avatar
integer points
varchar role
smallint status
jsonb properties
timestamp last_login_at
timestamp created_at
timestamp updated_at
}
```
图表来源
- [user.go](file://internal/model/user.go#L7-L21)
章节来源
- [user.go](file://internal/model/user.go#L7-L21)
- [user_service.go](file://internal/service/user_service.go#L14-L31)
### 错误码与响应格式
- 成功200返回结构化响应包含 token 与用户信息
- 参数错误400请求体绑定失败或字段校验失败
- 验证码错误400验证码不存在/过期/不匹配
- 资源冲突409用户名或邮箱已存在
- 服务器错误500内部异常
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
- [response.go](file://internal/model/response.go#L20-L73)
- [user_service.go](file://internal/service/user_service.go#L14-L31)
### 安全考虑与最佳实践
- 密码强度
- 至少6位建议结合复杂度策略字母、数字、特殊字符
- 使用 bcrypt 存储,避免明文或弱加密
- 邮箱格式验证
- 使用框架内置邮箱格式校验
- 防暴力注册
- 验证码发送频率限制1分钟
- 验证码有效期10分钟过期自动失效
- 唯一性检查防止重复注册
- 传输安全
- 建议启用 HTTPS避免明文传输
- 日志与审计
- 记录注册失败与异常行为,便于追踪
章节来源
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [verification_service.go](file://internal/service/verification_service.go#L40-L77)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L14-L31)
- [password.go](file://pkg/auth/password.go#L7-L21)
## 依赖关系分析
- 处理器依赖
- 验证码服务:用于校验注册验证码
- 用户服务执行注册流程唯一性检查、密码加密、创建用户、生成JWT
- JWT 服务:生成登录令牌
- 服务依赖
- 密码服务bcrypt 加密与校验
- 数据模型:用户实体
- 工具与中间件
- 路由:定义 /api/v1/auth/register
- 统一响应:封装业务码与消息
```mermaid
graph LR
Handler["处理器: Register"] --> VS["验证码服务: VerifyCode"]
Handler --> US["用户服务: RegisterUser"]
US --> PS["密码服务: HashPassword"]
US --> Model["用户模型: User"]
US --> JWT["JWT服务: GenerateToken"]
Handler --> Resp["统一响应: Response/ErrorResponse"]
Route["路由: /api/v1/auth/register"] --> Handler
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [user_service.go](file://internal/service/user_service.go#L12-L68)
- [password.go](file://pkg/auth/password.go#L7-L21)
- [user.go](file://internal/model/user.go#L7-L21)
- [response.go](file://internal/model/response.go#L20-L73)
- [routes.go](file://internal/handler/routes.go#L16-L26)
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
- [user_service.go](file://internal/service/user_service.go#L12-L68)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [password.go](file://pkg/auth/password.go#L7-L21)
- [user.go](file://internal/model/user.go#L7-L21)
- [response.go](file://internal/model/response.go#L20-L73)
- [routes.go](file://internal/handler/routes.go#L16-L26)
## 性能与扩展性
- 验证码缓存
- Redis 存储验证码与频率限制,具备高并发与低延迟特性
- 密码加密
- bcrypt 默认成本在安全性与性能间平衡,可根据硬件能力调整
- 扩展建议
- 引入速率限制中间件,对 /api/v1/auth/register 进一步限流
- 增加 IP/设备维度的风控策略
- 对高频失败场景增加验证码挑战或人机验证
[本节为通用建议,不直接分析具体文件]
## 故障排查指南
- 常见错误与定位
- 参数错误:检查请求体字段是否满足长度/格式要求
- 验证码错误:确认验证码是否过期、是否与发送邮箱一致
- 用户名/邮箱已存在:检查数据库唯一性约束
- 单元测试参考
- 注册请求体校验、错误处理与响应格式可通过测试用例验证
章节来源
- [auth_handler_test.go](file://internal/handler/auth_handler_test.go#L74-L116)
- [common_test.go](file://internal/types/common_test.go#L215-L287)
## 结论
用户注册 API 通过严格的请求体校验、验证码机制与 bcrypt 密码加密,提供了基础的安全保障。配合唯一性检查与频率限制,能够有效降低重复注册与暴力注册的风险。建议在生产环境中进一步完善风控策略与监控告警体系。
[本节为总结性内容,不直接分析具体文件]
## 附录
### 请求与响应示例(路径引用)
- 请求体字段与校验规则
- 字段定义与校验规则参见:[请求体定义](file://internal/types/common.go#L33-L40)
- 成功响应结构
- 成功响应结构参见:[统一响应](file://internal/model/response.go#L20-L73)
- 登录响应结构参见:[登录响应](file://internal/types/common.go#L107-L126)
- 错误响应结构
- 错误响应结构参见:[错误响应](file://internal/model/response.go#L20-L73)
- 路由与端点
- 注册路由参见:[路由定义](file://internal/handler/routes.go#L16-L26)
- 处理器与业务流程
- 注册处理器参见:[注册处理器](file://internal/handler/auth_handler.go#L27-L84)
- 注册业务流程参见:[注册服务](file://internal/service/user_service.go#L12-L68)
- 验证码与防暴力
- 验证码生成与校验参见:[验证码服务](file://internal/service/verification_service.go#L26-L98)
- 密码加密
- 密码加密与校验参见:[密码服务](file://pkg/auth/password.go#L7-L21)

View File

@@ -1,351 +0,0 @@
# 用户登录
<cite>
**本文引用的文件**
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [routes.go](file://internal/handler/routes.go)
- [jwt.go](file://pkg/auth/jwt.go)
- [password.go](file://pkg/auth/password.go)
- [user_service.go](file://internal/service/user_service.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [user.go](file://internal/model/user.go)
- [token.go](file://internal/model/token.go)
- [token_service.go](file://internal/service/token_service.go)
- [auth.go](file://internal/middleware/auth.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向“用户登录”API围绕 /api/v1/auth/login 端点进行完整说明,覆盖:
- HTTP 方法与路由
- 请求体结构(用户名或邮箱登录)
- 响应格式包含JWT token与用户信息
- 错误码与错误处理
- JWT令牌生成流程访问令牌与声明
- 登录成功/失败后的审计日志记录IP、User-Agent
- 安全建议(防暴力破解、错误提示模糊化)
## 项目结构
该登录流程涉及以下模块协作:
- 路由层:注册 /api/v1/auth/login 路由
- 控制器层:处理登录请求、参数校验、调用服务层
- 服务层:用户查找、密码校验、令牌生成、登录日志记录
- 认证与密码JWT服务、bcrypt密码加解密
- 数据模型:用户、登录日志、令牌
- 中间件JWT认证中间件用于后续受保护接口
```mermaid
graph TB
Client["客户端"] --> Routes["路由注册<br/>/api/v1/auth/login"]
Routes --> Handler["控制器 Login()<br/>参数绑定/错误处理"]
Handler --> Service["服务层 LoginUser()<br/>查找用户/校验密码/生成JWT"]
Service --> JWT["JWT服务 GenerateToken()<br/>签发访问令牌"]
Service --> ModelUser["用户模型 User"]
Service --> ModelLog["登录日志 UserLoginLog"]
Handler --> Resp["统一响应 Response/ErrorResponse"]
Client --> Middleware["认证中间件 AuthMiddleware()<br/>Bearer Token 校验"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L16-L25)
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [user.go](file://internal/model/user.go#L1-L21)
- [user.go](file://internal/model/user.go#L52-L71)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L25)
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
## 核心组件
- 路由与控制器
- /api/v1/auth/login 由控制器 Login 处理负责参数绑定、IP/User-Agent采集、调用服务层并返回统一响应。
- 服务层
- LoginUser 支持用户名或邮箱登录校验用户状态与密码生成JWT更新最后登录时间记录成功/失败日志。
- 认证与密码
- JWT服务提供 GenerateToken/ValidateToken密码使用 bcrypt 进行哈希与校验。
- 数据模型
- User用户信息UserLoginLog登录审计日志Token令牌模型用于后续刷新/失效等场景)。
- 统一响应
- Response/ErrorResponse 提供统一的状态码与消息封装。
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [password.go](file://pkg/auth/password.go#L7-L21)
- [user.go](file://internal/model/user.go#L1-L21)
- [user.go](file://internal/model/user.go#L52-L71)
- [token.go](file://internal/model/token.go#L1-L15)
- [response.go](file://internal/model/response.go#L20-L53)
## 架构总览
登录端到端序列图POST /api/v1/auth/login
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由"
participant H as "控制器 Login()"
participant S as "服务层 LoginUser()"
participant J as "JWT服务"
participant U as "用户模型"
participant L as "登录日志"
C->>R : "POST /api/v1/auth/login"
R->>H : "进入 Login 控制器"
H->>H : "ShouldBindJSON 绑定请求体"
H->>S : "LoginUser(jwtService, username, password, ip, ua)"
S->>U : "根据用户名或邮箱查找用户"
S->>S : "校验用户状态"
S->>S : "bcrypt 校验密码"
S->>J : "GenerateToken(userID, username, role)"
J-->>S : "返回JWT字符串"
S->>U : "更新 last_login_at"
S->>L : "logSuccessLogin/logFailedLogin"
S-->>H : "返回 user, token"
H-->>C : "200 成功响应或401失败响应"
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [user.go](file://internal/model/user.go#L1-L21)
- [user.go](file://internal/model/user.go#L52-L71)
## 详细组件分析
### 接口定义与请求/响应
- HTTP 方法与路径
- 方法POST
- 路径:/api/v1/auth/login
- 请求体结构
- 字段username支持用户名或邮箱、password6-128字符
- 参数校验:必填、长度约束
- 响应格式
- 成功统一响应data 包含 token 与 userInfo
- 失败:统一错误响应,包含 code 与 message
- 错误码
- 400请求参数错误
- 401未授权用户名/邮箱或密码错误、账号被禁用等)
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [common.go](file://internal/types/common.go#L27-L31)
- [response.go](file://internal/model/response.go#L20-L53)
### 请求体结构与参数校验
- 请求体字段
- username支持用户名或邮箱
- password6-128字符
- 参数校验
- Gin 绑定时会触发结构体 tag 校验(必填、长度范围)
- 实际行为
- 控制器层在绑定失败时直接返回400
章节来源
- [common.go](file://internal/types/common.go#L27-L31)
- [auth_handler.go](file://internal/handler/auth_handler.go#L101-L109)
### 登录流程与JWT生成
- 登录流程要点
- 识别登录方式:包含@视为邮箱,否则为用户名
- 校验用户状态(仅状态为正常才允许登录)
- 使用 bcrypt 校验密码
- 生成JWT访问令牌包含用户ID、用户名、角色等声明
- 更新最后登录时间
- 记录登录日志(成功/失败均记录IP与User-Agent
- JWT声明内容
- 包含 user_id、username、role
- 默认包含过期时间、签发时间、生效时间、签发者等注册声明
- 令牌有效期
- 由JWT服务配置的过期小时数决定
```mermaid
flowchart TD
Start(["进入 LoginUser"]) --> Detect["判断登录方式<br/>用户名或邮箱"]
Detect --> FindUser["查找用户"]
FindUser --> Status{"用户状态正常?"}
Status -- 否 --> LogFail["记录失败日志<br/>原因:账号被禁用"]
LogFail --> ReturnErr["返回错误"]
Status -- 是 --> CheckPwd["bcrypt 校验密码"]
CheckPwd --> PwdOK{"密码正确?"}
PwdOK -- 否 --> LogFail2["记录失败日志<br/>原因:密码错误"]
LogFail2 --> ReturnErr
PwdOK -- 是 --> GenToken["GenerateToken(userID, username, role)"]
GenToken --> UpdateTime["更新 last_login_at"]
UpdateTime --> LogSuccess["记录成功日志"]
LogSuccess --> Done(["返回 user, token"])
ReturnErr --> Done
```
图表来源
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [password.go](file://pkg/auth/password.go#L16-L21)
- [user.go](file://internal/model/user.go#L1-L21)
- [user.go](file://internal/model/user.go#L52-L71)
章节来源
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [password.go](file://pkg/auth/password.go#L16-L21)
### 响应格式与错误码
- 成功响应
- data包含 token 与 userInfoid、username、email、avatar、points、role、status、last_login_at、created_at、updated_at
- 失败响应
- 400请求参数错误
- 401未授权用户名/邮箱或密码错误、账号被禁用)
- 统一响应封装
- Response/ErrorResponse 提供 code、message、data/error 字段
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L131-L147)
- [common.go](file://internal/types/common.go#L107-L126)
- [response.go](file://internal/model/response.go#L20-L53)
### 登录日志记录IP、User-Agent
- 记录内容
- 成功/失败均记录用户ID、IP地址、User-Agent、登录方式PASSWORD、是否成功、失败原因
- 记录时机
- 成功:登录成功后记录
- 失败:用户不存在、账号禁用、密码错误等情况下记录
- 存储位置
- 写入 user_login_logs 表
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L111-L129)
- [user_service.go](file://internal/service/user_service.go#L203-L226)
- [user.go](file://internal/model/user.go#L52-L71)
### 安全考虑与建议
- 防暴力破解
- 建议在网关或应用层增加速率限制例如基于IP或用户名的限流在失败时延长冷却时间或临时封禁
- 可结合验证码机制(当前登录接口未强制验证码,但注册/重置密码有验证码流程)
- 错误提示模糊化
- 当前服务层对“用户不存在/密码错误/账号禁用”均返回统一的“用户名/邮箱或密码错误”,避免泄露具体原因
- 令牌管理
- 当前登录仅返回JWT访问令牌若需刷新令牌可参考后续令牌服务刷新/失效等)能力
- 传输安全
- 建议仅在HTTPS下提供登录接口防止凭据被窃听
章节来源
- [user_service.go](file://internal/service/user_service.go#L88-L103)
- [auth_handler.go](file://internal/handler/auth_handler.go#L116-L129)
## 依赖关系分析
- 控制器依赖
- 控制器 Login 依赖JWT服务、日志、Redis注册流程、服务层 LoginUser
- 服务层依赖
- LoginUser 依赖用户仓库查找用户、JWT服务生成令牌、密码工具bcrypt、登录日志仓库写入日志
- 认证依赖
- JWT服务依赖golang-jwt库密码工具依赖bcrypt
- 模型依赖
- User、UserLoginLog、Token 模型用于数据持久化与审计
```mermaid
graph LR
H["控制器 Login()"] --> S["服务层 LoginUser()"]
H --> J["JWT服务 GenerateToken()"]
S --> U["User 模型"]
S --> L["UserLoginLog 模型"]
S --> P["bcrypt 密码校验"]
H --> R["路由 RegisterRoutes()"]
M["认证中间件 AuthMiddleware()"] --> J
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [password.go](file://pkg/auth/password.go#L16-L21)
- [user.go](file://internal/model/user.go#L1-L21)
- [user.go](file://internal/model/user.go#L52-L71)
- [routes.go](file://internal/handler/routes.go#L16-L25)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [password.go](file://pkg/auth/password.go#L16-L21)
- [user.go](file://internal/model/user.go#L1-L21)
- [user.go](file://internal/model/user.go#L52-L71)
- [routes.go](file://internal/handler/routes.go#L16-L25)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
## 性能考量
- 登录路径涉及数据库查询与密码校验,建议:
- 对用户表建立合适的索引username、email
- 密码校验使用 bcrypt默认成本适中可根据硬件能力调整
- 登录日志写入采用异步或批量策略当前为同步写入注意高并发下的I/O影响
- JWT生成与校验为CPU密集度较低的操作主要瓶颈在数据库与密码校验
## 故障排查指南
- 常见问题与定位
- 400 参数错误:检查请求体字段是否缺失或长度不符合要求
- 401 未授权:核对用户名/邮箱是否存在、账号状态是否正常、密码是否正确
- 登录日志未记录:确认日志写入是否成功,检查数据库连接与表结构
- 日志与审计
- 成功/失败日志均包含 IP 与 User-Agent便于追踪来源设备与来源网络
- 令牌相关
- 若后续引入刷新令牌,可参考令牌服务中的刷新/失效逻辑
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L101-L129)
- [user_service.go](file://internal/service/user_service.go#L203-L226)
- [response.go](file://internal/model/response.go#L20-L53)
## 结论
/api/v1/auth/login 提供了完整的用户名或邮箱登录能力具备参数校验、密码校验、JWT签发与登录日志记录。当前实现聚焦于登录流程本身后续可在速率限制、验证码、刷新令牌等方面进一步增强安全性与可用性。
## 附录
### API定义与示例
- 端点
- 方法POST
- 路径:/api/v1/auth/login
- 请求体
- username字符串必填支持用户名或邮箱
- password字符串必填长度6-128
- 成功响应
- code200
- data包含 token 与 userInfo
- 示例(结构示意)
- data: { token: "...", userInfo: { id, username, email, avatar, points, role, status, last_login_at, created_at, updated_at } }
- 失败响应
- 400请求参数错误
- 401未授权用户名/邮箱或密码错误、账号被禁用)
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [common.go](file://internal/types/common.go#L27-L31)
- [common.go](file://internal/types/common.go#L107-L126)
- [response.go](file://internal/model/response.go#L20-L53)
### JWT声明与令牌生命周期
- 声明内容
- user_id、username、role
- 过期时间、签发时间、生效时间、签发者等注册声明
- 令牌类型
- 当前登录返回访问令牌;刷新/失效等能力可参考令牌服务
章节来源
- [jwt.go](file://pkg/auth/jwt.go#L24-L53)
- [token_service.go](file://internal/service/token_service.go#L151-L238)

View File

@@ -1,460 +0,0 @@
# 认证API
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [jwt.go](file://pkg/auth/jwt.go)
- [password.go](file://pkg/auth/password.go)
- [user_service.go](file://internal/service/user_service.go)
- [verification_service.go](file://internal/service/verification_service.go)
- [captcha_handler.go](file://internal/handler/captcha_handler.go)
- [captcha_service.go](file://internal/service/captcha_service.go)
- [auth.go](file://internal/middleware/auth.go)
- [common.go](file://internal/types/common.go)
- [email.go](file://pkg/email/email.go)
- [config.go](file://pkg/config/config.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者系统性梳理认证API的设计与实现覆盖用户注册、登录、发送验证码与重置密码四个核心端点。文档基于路由分组与处理器实现详细说明每个API的HTTP方法、请求参数、响应格式与错误码重点解释JWT认证机制在登录流程中的作用以及验证码服务与邮箱服务的集成方式并提供实际请求/响应示例路径帮助快速理解认证流程。同时总结安全要点包括密码加密存储、JWT过期策略与防暴力破解措施。
## 项目结构
认证相关能力由“路由层-处理器层-服务层-基础设施层”四层构成:
- 路由层:在统一的路由组下挂载认证相关端点
- 处理器层:负责参数绑定、调用服务、组织响应与错误处理
- 服务层:封装业务逻辑(用户注册/登录、验证码发送与校验、密码变更等)
- 基础设施层JWT签发与校验、密码加解密、Redis验证码存储、SMTP邮件发送
```mermaid
graph TB
subgraph "路由层"
R["routes.go<br/>注册/auth路由组"]
end
subgraph "处理器层"
AH["auth_handler.go<br/>注册/登录/发送验证码/重置密码"]
CH["captcha_handler.go<br/>图形验证码生成/校验"]
end
subgraph "服务层"
US["user_service.go<br/>注册/登录/密码重置等"]
VS["verification_service.go<br/>验证码生成/发送/校验"]
CS["captcha_service.go<br/>图形验证码生成/校验"]
end
subgraph "基础设施层"
JWT["jwt.go<br/>JWT签发/校验"]
PW["password.go<br/>bcrypt加解密"]
EM["email.go<br/>SMTP邮件发送"]
CFG["config.go<br/>JWT/邮件/Redis等配置"]
end
R --> AH
R --> CH
AH --> US
AH --> VS
CH --> CS
US --> JWT
US --> PW
VS --> EM
VS --> CFG
CS --> CFG
```
图表来源
- [routes.go](file://internal/handler/routes.go#L16-L25)
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L249)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
- [user_service.go](file://internal/service/user_service.go#L12-L122)
- [verification_service.go](file://internal/service/verification_service.go#L14-L119)
- [captcha_service.go](file://internal/service/captcha_service.go#L18-L166)
- [jwt.go](file://pkg/auth/jwt.go#L10-L71)
- [password.go](file://pkg/auth/password.go#L1-L21)
- [email.go](file://pkg/email/email.go#L15-L163)
- [config.go](file://pkg/config/config.go#L67-L107)
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L25)
## 核心组件
- 路由组与端点
- /api/v1/auth/registerPOST注册
- /api/v1/auth/loginPOST登录
- /api/v1/auth/send-codePOST发送验证码
- /api/v1/auth/reset-passwordPOST重置密码
- 中间件
- 认证中间件对受保护资源进行JWT校验
- 关键数据模型
- 请求/响应结构体定义于类型模块,包含登录、注册、验证码发送与重置密码等请求体与响应体字段
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L25)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [common.go](file://internal/types/common.go#L27-L61)
## 架构总览
认证API采用“路由-处理器-服务-基础设施”的分层设计,认证流程的关键交互如下:
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "处理器(auth_handler)"
participant S as "服务(user_service/verification_service)"
participant J as "JWT服务(pkg/auth)"
participant E as "邮件服务(pkg/email)"
participant R as "Redis"
rect rgb(255,255,255)
Note over C,H : 注册流程
C->>H : POST /api/v1/auth/register
H->>S : VerifyCode(验证码校验)
S->>R : GET verification : code : register : email
R-->>S : 验证码
S-->>H : 校验通过
H->>S : RegisterUser(创建用户+生成JWT)
S->>J : GenerateToken(userID, username, role)
J-->>S : token
S-->>H : 用户信息+token
H-->>C : 成功响应(包含token与用户信息)
end
rect rgb(255,255,255)
Note over C,H : 登录流程
C->>H : POST /api/v1/auth/login
H->>S : LoginUser(用户名/邮箱+密码)
S->>J : GenerateToken(userID, username, role)
J-->>S : token
S-->>H : 用户信息+token
H-->>C : 成功响应(包含token与用户信息)
end
rect rgb(255,255,255)
Note over C,H : 发送验证码流程
C->>H : POST /api/v1/auth/send-code
H->>S : SendVerificationCode(生成+存储+限流+发邮件)
S->>R : SET verification : code : type : email
S->>E : 发送邮件
E-->>S : 发送结果
S-->>H : 成功
H-->>C : 成功响应
end
rect rgb(255,255,255)
Note over C,H : 重置密码流程
C->>H : POST /api/v1/auth/reset-password
H->>S : VerifyCode(验证码校验)
S->>R : GET verification : code : reset_password : email
R-->>S : 验证码
S-->>H : 校验通过
H->>S : ResetUserPassword(更新密码)
S-->>H : 成功
H-->>C : 成功响应
end
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L249)
- [user_service.go](file://internal/service/user_service.go#L12-L122)
- [verification_service.go](file://internal/service/verification_service.go#L40-L98)
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [email.go](file://pkg/email/email.go#L29-L55)
- [config.go](file://pkg/config/config.go#L67-L107)
## 详细组件分析
### 路由与端点
- 路由组
- /api/v1/auth认证相关端点
- /api/v1/user受JWT保护的用户资料端点不在本次文档范围
- 端点清单
- POST /api/v1/auth/register注册
- POST /api/v1/auth/login登录
- POST /api/v1/auth/send-code发送验证码
- POST /api/v1/auth/reset-password重置密码
章节来源
- [routes.go](file://internal/handler/routes.go#L16-L25)
### 注册接口POST /api/v1/auth/register
- 功能概述
- 接收用户名、邮箱、密码、验证码与可选头像校验验证码后创建用户并签发JWT
- 请求参数
- 字段username、email、password、verification_code、avatar
- 校验规则用户名长度、邮箱格式、密码长度、验证码长度、头像URL格式
- 响应格式
- 成功包含token与用户信息
- 失败:错误码与错误信息
- 错误码
- 400请求参数错误、验证码错误、用户名/邮箱已存在、密码加密失败、生成Token失败
- 安全要点
- 密码经bcrypt加密存储
- 验证码有效期短且一次性使用(校验通过即删除)
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
- [user_service.go](file://internal/service/user_service.go#L12-L68)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [password.go](file://pkg/auth/password.go#L1-L21)
- [common.go](file://internal/types/common.go#L33-L40)
### 登录接口POST /api/v1/auth/login
- 功能概述
- 支持用户名或邮箱登录校验密码后签发JWT并记录登录日志
- 请求参数
- 字段username支持用户名或邮箱、password
- 校验规则:必填、长度范围
- 响应格式
- 成功包含token与用户信息
- 失败401未授权
- 错误码
- 400请求参数错误
- 401用户名/邮箱或密码错误、账号被禁用
- 安全要点
- 密码使用bcrypt校验
- 登录成功更新最后登录时间
- 失败场景记录登录日志
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [password.go](file://pkg/auth/password.go#L16-L21)
- [common.go](file://internal/types/common.go#L27-L31)
### 发送验证码接口POST /api/v1/auth/send-code
- 功能概述
- 根据邮箱与类型(注册/重置密码/更换邮箱生成6位数字验证码存储至Redis并按类型限流随后通过SMTP发送邮件
- 请求参数
- 字段email、type枚举register/reset_password/change_email
- 校验规则邮箱格式、type枚举
- 响应格式
- 成功:通用成功响应
- 失败:错误码与错误信息
- 错误码
- 400请求参数错误、发送过于频繁、验证码已过期或不存在、验证码错误
- 安全要点
- 验证码有效期10分钟
- 发送频率限制1分钟/次
- 邮件服务可开关,未启用时跳过发送
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L14-L119)
- [email.go](file://pkg/email/email.go#L29-L55)
- [common.go](file://internal/types/common.go#L49-L54)
- [config.go](file://pkg/config/config.go#L67-L107)
### 重置密码接口POST /api/v1/auth/reset-password
- 功能概述
- 通过邮箱验证码校验后,更新用户密码
- 请求参数
- 字段email、verification_code、new_password
- 校验规则:邮箱格式、验证码长度、新密码长度
- 响应格式
- 成功:通用成功响应
- 失败400或500
- 错误码
- 400请求参数错误、验证码错误
- 500内部错误如用户不存在、密码加密失败
- 安全要点
- 密码经bcrypt加密存储
- 验证码一次性使用(校验通过即删除)
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
- [user_service.go](file://internal/service/user_service.go#L166-L184)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [password.go](file://pkg/auth/password.go#L1-L21)
- [common.go](file://internal/types/common.go#L55-L61)
### JWT认证机制登录流程
- 生成与签发
- 登录成功后服务层调用JWT服务生成token包含用户ID、用户名、角色及标准声明过期时间、签发时间等
- 校验与中间件
- 中间件从Authorization头提取Bearer token并调用JWT服务校验校验通过后将用户信息写入上下文
- 过期策略
- JWT过期小时数来自配置默认值可通过环境变量设置
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "处理器(Login)"
participant S as "服务(LoginUser)"
participant J as "JWT服务"
participant M as "认证中间件"
C->>H : POST /api/v1/auth/login
H->>S : LoginUser(usernameOrEmail, password)
S->>J : GenerateToken(userID, username, role)
J-->>S : token
S-->>H : 用户信息+token
H-->>C : {token, user_info}
Note over C,M : 访问受保护资源
C->>M : 携带Authorization : Bearer token
M->>J : ValidateToken(token)
J-->>M : Claims
M-->>C : 放行
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
- [user_service.go](file://internal/service/user_service.go#L70-L122)
- [jwt.go](file://pkg/auth/jwt.go#L32-L71)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [config.go](file://pkg/config/config.go#L67-L71)
章节来源
- [jwt.go](file://pkg/auth/jwt.go#L10-L71)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [config.go](file://pkg/config/config.go#L67-L71)
### 验证码服务与邮箱服务集成
- 验证码生成与存储
- 生成6位数字验证码存储于Redis键名包含类型与邮箱有效期10分钟
- 发送频率限制1分钟/次,避免刷屏
- 邮件发送
- 根据类型选择不同主题与正文模板支持465/587端口TLS/STARTTLS
- 邮件服务可开关,未启用时跳过发送
- 图形验证码captcha
- 生成滑块拼图验证码主图与滑块图以Base64形式返回目标坐标保存在Redis验证时比较偏移容差
```mermaid
flowchart TD
Start(["开始"]) --> GenCode["生成6位数字验证码"]
GenCode --> StoreRedis["存储到Redis<br/>键: verification:code:{type}:{email}<br/>过期: 10分钟"]
StoreRedis --> RateLimit["设置发送频率限制<br/>键: verification:rate_limit:{type}:{email}<br/>过期: 1分钟"]
RateLimit --> SendMail["发送邮件(根据类型选择模板)"]
SendMail --> MailOK{"邮件发送成功?"}
MailOK --> |是| Done(["结束"])
MailOK --> |否| Clean["删除Redis验证码键"] --> Fail(["失败"])
subgraph "图形验证码"
GStart["生成滑块拼图验证码"] --> SaveCaptcha["保存目标坐标到Redis<br/>键: captcha:{id}<br/>过期: 5分钟"]
SaveCaptcha --> ReturnBase64["返回主图/滑块图Base64与captchaId"]
Verify["前端提交dx与captchaId"] --> LoadCaptcha["从Redis读取目标坐标"]
LoadCaptcha --> Compare["比较dx与目标坐标容差"]
Compare --> |通过| DelCaptcha["删除Redis记录"]
Compare --> |失败| Warn["返回失败"]
end
```
图表来源
- [verification_service.go](file://internal/service/verification_service.go#L26-L119)
- [email.go](file://pkg/email/email.go#L29-L105)
- [captcha_service.go](file://internal/service/captcha_service.go#L75-L166)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
章节来源
- [verification_service.go](file://internal/service/verification_service.go#L14-L119)
- [email.go](file://pkg/email/email.go#L29-L105)
- [captcha_service.go](file://internal/service/captcha_service.go#L18-L166)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
### 请求/响应示例(示例路径)
- 注册
- 请求POST /api/v1/auth/register
- 示例路径:[注册请求体定义](file://internal/types/common.go#L33-L40)
- 响应包含token与用户信息
- 登录
- 请求POST /api/v1/auth/login
- 示例路径:[登录请求体定义](file://internal/types/common.go#L27-L31)
- 响应包含token与用户信息
- 发送验证码
- 请求POST /api/v1/auth/send-code
- 示例路径:[发送验证码请求体定义](file://internal/types/common.go#L49-L54)
- 响应:通用成功响应
- 重置密码
- 请求POST /api/v1/auth/reset-password
- 示例路径:[重置密码请求体定义](file://internal/types/common.go#L55-L61)
- 响应:通用成功响应
章节来源
- [common.go](file://internal/types/common.go#L27-L61)
## 依赖关系分析
- 组件耦合
- 处理器依赖服务层服务层依赖JWT与密码工具、Redis与邮件服务
- 中间件依赖JWT服务统一拦截受保护资源
- 外部依赖
- Redis验证码存储与限流
- SMTP邮件发送
- JWT令牌签发与校验
- 循环依赖
- 未见循环依赖迹象
```mermaid
graph LR
AH["auth_handler.go"] --> US["user_service.go"]
AH --> VS["verification_service.go"]
CH["captcha_handler.go"] --> CS["captcha_service.go"]
US --> JWT["jwt.go"]
US --> PW["password.go"]
VS --> EM["email.go"]
VS --> CFG["config.go"]
CS --> CFG
M["auth.go"] --> JWT
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L249)
- [user_service.go](file://internal/service/user_service.go#L12-L122)
- [verification_service.go](file://internal/service/verification_service.go#L14-L119)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
- [captcha_service.go](file://internal/service/captcha_service.go#L18-L166)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [jwt.go](file://pkg/auth/jwt.go#L10-L71)
- [password.go](file://pkg/auth/password.go#L1-L21)
- [email.go](file://pkg/email/email.go#L15-L163)
- [config.go](file://pkg/config/config.go#L67-L107)
## 性能考量
- Redis命中率
- 验证码键短生命周期建议合理设置Redis内存与淘汰策略避免热键阻塞
- 邮件发送
- 异步化与重试策略可进一步优化(当前实现为同步发送),避免阻塞请求线程
- JWT负载
- Claims中仅包含必要字段减少token体积提升网络传输效率
- 登录日志
- 失败日志写入数据库可能带来IO压力建议结合异步队列或批量写入
## 故障排查指南
- 登录失败
- 现象401未授权
- 排查:确认用户名/邮箱是否存在、密码是否正确、账号状态是否正常
- 参考路径:[登录失败处理](file://internal/service/user_service.go#L84-L104)
- 验证码错误/过期
- 现象400错误
- 排查:确认验证码类型与邮箱匹配、是否在有效期内、是否被重复使用
- 参考路径:[验证码校验](file://internal/service/verification_service.go#L79-L98)
- 邮件发送失败
- 现象:发送验证码接口报错
- 排查检查邮件服务开关、SMTP配置、网络连通性
- 参考路径:[邮件发送](file://pkg/email/email.go#L29-L105)
- JWT无效
- 现象访问受保护资源401
- 排查确认Authorization头格式、token是否过期、签名是否正确
- 参考路径:[JWT校验](file://pkg/auth/jwt.go#L55-L71)
章节来源
- [user_service.go](file://internal/service/user_service.go#L84-L104)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [email.go](file://pkg/email/email.go#L29-L105)
- [jwt.go](file://pkg/auth/jwt.go#L55-L71)
## 结论
该认证API围绕“路由-处理器-服务-基础设施”清晰分层实现了完整的注册、登录、验证码与重置密码流程。JWT用于无状态鉴权bcrypt保障密码安全Redis与SMTP分别承担验证码与邮件能力。通过严格的参数校验、限流与一次性验证码使用策略有效提升了安全性与可用性。建议后续引入图形验证码与更完善的限流策略进一步增强抗暴力破解能力。
## 附录
- 安全最佳实践
- 密码加密始终使用bcrypt
- JWT过期合理设置过期时间短期会话建议缩短
- 验证码:短有效期、一次性使用、限流
- 传输安全生产环境强制HTTPS
- 日志脱敏:避免泄露敏感信息
- 环境变量与配置
- JWT密钥与过期小时数、邮件SMTP配置、Redis连接参数等均通过环境变量注入
章节来源
- [config.go](file://pkg/config/config.go#L67-L107)

View File

@@ -1,343 +0,0 @@
# 重置密码
<cite>
**本文引用的文件**
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
- [internal/handler/routes.go](file://internal/handler/routes.go)
- [internal/service/verification_service.go](file://internal/service/verification_service.go)
- [internal/service/user_service.go](file://internal/service/user_service.go)
- [pkg/auth/password.go](file://pkg/auth/password.go)
- [internal/types/common.go](file://internal/types/common.go)
- [internal/model/response.go](file://internal/model/response.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者与测试人员系统化梳理“重置密码”API的完整实现与使用规范覆盖以下要点
- 接口定位与HTTP方法POST /api/v1/auth/reset-password
- 请求体字段与校验规则email、verificationCode、newPassword
- 响应格式与错误码:统一的业务状态码与错误消息
- 流程说明:验证码验证 → 密码加密更新 → 成功响应
- 安全考虑:验证码一次性使用、有效期控制、密码复杂度要求
## 项目结构
围绕“重置密码”的关键文件分布如下:
- 路由注册:在路由组中将 /api/v1/auth/reset-password 绑定到处理器
- 处理器:接收请求、绑定参数、调用服务层、返回统一响应
- 服务层:
- 验证码服务:生成、发送、验证、删除;验证码一次性使用与有效期控制
- 用户服务:根据邮箱查找用户并更新密码(密码经加密后持久化)
- 工具与模型:
- 密码工具bcrypt 加密与校验
- 请求类型ResetPasswordRequest 的字段与约束
- 响应模型:统一响应结构与常用状态码
```mermaid
graph TB
subgraph "接口层"
R["路由注册<br/>/api/v1/auth/reset-password"]
H["处理器<br/>ResetPassword"]
end
subgraph "服务层"
VS["验证码服务<br/>VerifyCode/Generate/Send/Delete"]
US["用户服务<br/>ResetUserPassword"]
end
subgraph "工具与模型"
PW["密码工具<br/>bcrypt 加密/校验"]
RT["请求类型<br/>ResetPasswordRequest"]
RM["响应模型<br/>统一响应/状态码"]
end
R --> H
H --> VS
H --> US
US --> PW
H --> RM
VS --> RM
RT --> H
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L26)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L98)
- [internal/service/user_service.go](file://internal/service/user_service.go#L166-L184)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [internal/types/common.go](file://internal/types/common.go#L55-L61)
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L26)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
## 核心组件
- 接口路径与方法
- 方法POST
- 路径:/api/v1/auth/reset-password
- 请求体结构
- 字段:
- email字符串必填需符合邮箱格式
- verificationCode字符串必填长度必须为6
- newPassword字符串必填长度范围为6~128
- 校验来源:请求类型定义与处理器绑定校验
- 响应格式
- 成功统一响应结构code=200message=“操作成功”data为空或简要提示
- 失败:统一错误响应结构,包含业务状态码与错误消息
- 错误码
- 400请求参数错误如字段缺失、格式不符、长度不符
- 400验证码错误或已过期
- 500服务端内部错误如数据库异常
章节来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
- [internal/types/common.go](file://internal/types/common.go#L55-L61)
- [internal/model/response.go](file://internal/model/response.go#L27-L52)
## 架构总览
下图展示“重置密码”端到端调用链路与各模块职责。
```mermaid
sequenceDiagram
participant C as "客户端"
participant G as "Gin路由"
participant H as "处理器 ResetPassword"
participant VS as "验证码服务 VerifyCode"
participant US as "用户服务 ResetUserPassword"
participant PW as "密码工具 bcrypt"
participant DB as "数据库"
C->>G : "POST /api/v1/auth/reset-password"
G->>H : "路由转发"
H->>H : "绑定并校验请求体"
H->>VS : "VerifyCode(email, code, type=reset_password)"
VS-->>H : "验证通过/失败"
alt "验证码失败"
H-->>C : "400 错误响应"
else "验证码通过"
H->>US : "ResetUserPassword(email, newPassword)"
US->>PW : "HashPassword(newPassword)"
PW-->>US : "hashedPassword"
US->>DB : "UpdateUserFields(password=hashed)"
DB-->>US : "OK"
US-->>H : "OK"
H-->>C : "200 成功响应"
end
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [internal/service/user_service.go](file://internal/service/user_service.go#L166-L184)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
## 详细组件分析
### 处理器ResetPassword
- 职责
- 绑定并校验请求体
- 调用验证码服务验证验证码(一次性使用)
- 调用用户服务更新密码(密码加密存储)
- 返回统一响应
- 关键行为
- 参数绑定失败返回400
- 验证码失败返回400
- 更新密码失败返回500
- 成功返回200
章节来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
### 验证码服务VerifyCode 与发送流程
- 验证码类型
- reset_password用于重置密码场景
- 一次性使用
- 验证通过后立即删除Redis中的验证码键值
- 有效期控制
- 默认10分钟
- 发送频率限制
- 同一邮箱同类型验证码在1分钟内不可重复发送
- 发送流程
- 生成6位数字验证码
- 写入Redis带过期时间
- 设置发送频率限制键
- 发送邮件(按类型区分模板)
```mermaid
flowchart TD
Start(["开始"]) --> Bind["绑定请求体"]
Bind --> Verify["VerifyCode(email, code, type)"]
Verify --> Check{"验证码正确且未过期?"}
Check --> |否| Err["返回400 错误"]
Check --> |是| Delete["删除Redis中的验证码键"]
Delete --> Next["进入下一步"]
Err --> End(["结束"])
Next --> End
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L218-L230)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L79-L98)
章节来源
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L98)
### 用户服务ResetUserPassword
- 流程
- 根据邮箱查询用户
- 对新密码进行bcrypt加密
- 更新用户记录的password字段
- 错误处理
- 用户不存在:返回错误
- 加密失败:返回错误
- 数据库更新失败:返回错误
```mermaid
flowchart TD
S(["开始"]) --> Find["根据邮箱查找用户"]
Find --> Found{"找到用户?"}
Found --> |否| E1["返回错误:用户不存在"]
Found --> |是| Hash["HashPassword(newPassword)"]
Hash --> HOK{"加密成功?"}
HOK --> |否| E2["返回错误:密码加密失败"]
HOK --> |是| Update["UpdateUserFields(password=hashed)"]
Update --> UOK{"更新成功?"}
UOK --> |否| E3["返回错误:数据库更新失败"]
UOK --> |是| OK["返回成功"]
E1 --> End(["结束"])
E2 --> End
E3 --> End
OK --> End
```
图表来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L166-L184)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L166-L184)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
### 请求体与响应模型
- 请求体字段与约束
- email必填邮箱格式
- verificationCode必填长度6
- newPassword必填长度6~128
- 响应模型
- 成功code=200message=“操作成功”
- 失败code=400或500message=错误描述
章节来源
- [internal/types/common.go](file://internal/types/common.go#L55-L61)
- [internal/model/response.go](file://internal/model/response.go#L27-L52)
## 依赖关系分析
- 路由到处理器
- /api/v1/auth/reset-password → ResetPassword
- 处理器到服务层
- ResetPassword → VerifyCode验证码验证
- ResetPassword → ResetUserPassword密码更新
- 服务层到工具
- ResetUserPassword → HashPasswordbcrypt加密
- 服务层到存储
- ResetUserPassword → UpdateUserFields持久化
```mermaid
graph LR
Routes["路由"] --> Handler["处理器 ResetPassword"]
Handler --> VS["验证码服务"]
Handler --> US["用户服务"]
US --> PW["密码工具 bcrypt"]
US --> DB["数据库"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L26)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [internal/service/user_service.go](file://internal/service/user_service.go#L166-L184)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L26)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L79-L98)
- [internal/service/user_service.go](file://internal/service/user_service.go#L166-L184)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
## 性能考量
- 验证码存储与访问
- Redis作为验证码缓存具备高并发读写能力建议合理设置过期时间与限流键避免热点攻击
- 密码加密成本
- bcrypt默认成本较高单次加密有一定CPU开销建议在高并发场景下关注服务实例的CPU占用与延迟
- 数据库更新
- 单字段更新通常很快;建议确保数据库连接池配置合理,避免阻塞
## 故障排查指南
- 常见错误与定位
- 400 参数错误:检查请求体字段是否齐全、格式是否正确、长度是否满足约束
- 400 验证码错误/过期:确认验证码是否正确、是否已被使用(一次性)、是否超过有效期
- 500 服务器错误:检查用户是否存在、密码加密是否成功、数据库更新是否异常
- 日志与追踪
- 处理器对失败场景会输出警告/错误日志,便于定位问题
- 验证码生命周期
- 验证通过即删除,确保验证码只能使用一次
- 过期时间为10分钟超过后无法再使用
章节来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L218-L249)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L79-L98)
## 结论
“重置密码”API以清晰的职责划分与严格的参数校验保障了安全性与可用性。验证码一次性使用与有效期控制有效降低了滥用风险密码采用bcrypt加密存储符合安全最佳实践。统一的响应模型与错误码体系提升了接口一致性与可观测性。
## 附录
### 请求与响应示例
- 请求示例JSON
- POST /api/v1/auth/reset-password
- Body
- email字符串必填邮箱格式
- verificationCode字符串必填长度6
- newPassword字符串必填长度6~128
- 成功响应示例JSON
- Status200
- Body
- code200
- message“操作成功”
- data空对象或简要提示
- 失败响应示例JSON
- Status400 或 500
- Body
- code400 或 500
- message错误描述
- error详细错误信息开发环境
章节来源
- [internal/types/common.go](file://internal/types/common.go#L55-L61)
- [internal/model/response.go](file://internal/model/response.go#L27-L52)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L218-L249)
### 安全考虑
- 验证码一次性使用
- 验证通过后立即删除Redis中的验证码键防止复用
- 验证码有效期控制
- 默认10分钟超时即失效
- 发送频率限制
- 同一邮箱同类型验证码在1分钟内不可重复发送
- 密码复杂度要求
- newPassword长度范围为6~128建议结合业务策略进一步增强复杂度例如包含大小写字母、数字与特殊字符当前实现以长度约束为主
章节来源
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L14-L24)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L98)
- [internal/types/common.go](file://internal/types/common.go#L55-L61)

View File

@@ -1,294 +0,0 @@
# 验证码API
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [captcha_handler.go](file://internal/handler/captcha_handler.go)
- [captcha_service.go](file://internal/service/captcha_service.go)
- [redis.go](file://pkg/redis/redis.go)
- [response.go](file://internal/model/response.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [captcha_handler_test.go](file://internal/handler/captcha_handler_test.go)
- [captcha_service_test.go](file://internal/service/captcha_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向验证码服务API的使用者与维护者围绕路由组“/api/v1/captcha”下的两个核心端点
- GET /generate生成图形验证码滑块拼图返回主图、滑块图、验证码唯一标识及目标Y坐标等信息。
- POST /verify验证用户提交的滑动偏移量判断是否在容差范围内并在验证通过后清理缓存。
文档将详细说明验证码的生成机制基于滑块拼图算法、有效期管理Redis过期策略、验证流程与状态处理并给出请求/响应示例。同时,说明该服务如何与注册、登录、更换邮箱等功能集成以提升安全性。
## 项目结构
验证码API位于路由组“/api/v1/captcha”由处理器与服务层共同实现底层依赖Redis进行验证码数据的临时存储与过期控制。
```mermaid
graph TB
subgraph "路由层"
R["routes.go<br/>注册 /api/v1/captcha/* 路由"]
end
subgraph "处理器层"
GH["captcha_handler.go<br/>Generate/Verify"]
end
subgraph "服务层"
CS["captcha_service.go<br/>GenerateCaptchaData/VerifyCaptchaData"]
end
subgraph "基础设施"
RC["redis.go<br/>Client 封装"]
end
R --> GH
GH --> CS
CS --> RC
```
图表来源
- [routes.go](file://internal/handler/routes.go#L80-L86)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
章节来源
- [routes.go](file://internal/handler/routes.go#L80-L86)
## 核心组件
- 路由注册:在路由组“/api/v1/captcha”下注册“/generate”和“/verify”两个端点。
- 处理器:负责参数绑定、调用服务层、组织响应。
- 服务层负责验证码生成滑块拼图、目标坐标提取、Redis存储与过期设置、验证偏移量、容差判断、验证后清理。
- Redis客户端提供Set/Get/Del等基础能力封装错误处理。
章节来源
- [routes.go](file://internal/handler/routes.go#L80-L86)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
## 架构总览
验证码API采用“路由 -> 处理器 -> 服务 -> Redis”的分层设计。生成阶段将目标坐标与容差信息写入Redis并设置过期时间验证阶段读取目标坐标计算容差并判定有效性通过后立即删除对应键防止重复使用。
```mermaid
sequenceDiagram
participant C as "客户端"
participant G as "Generate 处理器"
participant S as "验证码服务"
participant R as "Redis 客户端"
C->>G : GET /api/v1/captcha/generate
G->>S : GenerateCaptchaData(ctx, redisClient)
S->>S : 生成滑块拼图数据
S->>R : Set(key=captcha : id, value={tx,ty}, expire=5m)
S-->>G : 返回 masterImage, tileImage, captchaId, y
G-->>C : JSON {code,data{masterImage,tileImage,captchaId,y}}
```
图表来源
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L12-L34)
- [captcha_service.go](file://internal/service/captcha_service.go#L76-L135)
- [redis.go](file://pkg/redis/redis.go#L60-L83)
章节来源
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L12-L34)
- [captcha_service.go](file://internal/service/captcha_service.go#L76-L135)
## 详细组件分析
### 路由与端点
- 路由组“/api/v1/captcha”
- GET /generate生成验证码数据主图、滑块图、验证码ID、目标Y坐标
- POST /verify验证用户提交的滑动偏移量。
章节来源
- [routes.go](file://internal/handler/routes.go#L80-L86)
### 生成验证码GET /generate
- 输入:无显式请求体。
- 输出:包含以下字段的数据对象
- masterImage主图base64字符串
- tileImage滑块图base64字符串
- captchaId验证码唯一标识用于后续验证
- y目标Y坐标前端可据此定位滑块初始位置
- 内部流程要点
- 生成唯一ID作为验证码会话标识。
- 生成滑块拼图数据提取目标X/Y坐标。
- 将目标坐标序列化后写入Redis键名前缀为“captcha:”过期时间为300秒5分钟
- 返回主图、滑块图、captchaId与修正后的Y坐标给前端。
请求/响应示例(路径)
- 请求GET /api/v1/captcha/generate
- 响应JSON
- code200
- data.masterImage主图base64
- data.tileImage滑块图base64
- data.captchaId验证码ID
- data.y目标Y坐标
章节来源
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L12-L34)
- [captcha_service.go](file://internal/service/captcha_service.go#L76-L135)
### 验证验证码POST /verify
- 请求体字段
- captchaId必填验证码唯一标识
- dx必填用户滑动的X轴偏移量
- 验证逻辑
- 从Redis读取目标坐标tx, ty
- 使用容差值默认±3像素判断 dx 是否在 [tx-3, tx+3] 范围内。
- 若通过删除Redis中的验证码记录返回成功若失败返回失败。
- 错误处理
- 参数缺失或格式错误返回400。
- Redis查询失败或键不存在返回500或提示“验证码已过期或无效”。
请求/响应示例(路径)
- 请求POST /api/v1/captcha/generate
- JSON{ "captchaId": "...", "dx": 123 }
- 响应:
- 成功JSON { "code": 200, "msg": "验证成功" }
- 失败JSON { "code": 400, "msg": "验证失败,请重试" }
章节来源
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L36-L76)
- [captcha_service.go](file://internal/service/captcha_service.go#L137-L166)
### 验证流程与容差机制
```mermaid
flowchart TD
Start(["开始"]) --> Read["读取Redis中的目标坐标(tx, ty)"]
Read --> Found{"键是否存在?"}
Found --> |否| Expired["返回错误:验证码已过期或无效"]
Found --> |是| Calc["计算 |dx - tx|"]
Calc --> Check{"是否 ≤ 容差(3)"}
Check --> |是| Delete["删除Redis键"]
Delete --> Pass["返回验证成功"]
Check --> |否| Fail["返回验证失败"]
Expired --> End(["结束"])
Pass --> End
Fail --> End
```
图表来源
- [captcha_service.go](file://internal/service/captcha_service.go#L137-L166)
章节来源
- [captcha_service.go](file://internal/service/captcha_service.go#L137-L166)
### 与注册/登录/更换邮箱的安全集成
- 注册(/api/v1/auth/register
- 在注册流程中,前端需先获取图形验证码并完成验证,再提交邮箱验证码(通过 /api/v1/auth/send-code 发送)。
- 后端在注册接口处验证邮箱验证码的有效性,确保注册过程安全。
- 登录(/api/v1/auth/login
- 登录流程通常不强制要求图形验证码但可在高风险场景如异常IP、频繁尝试引入图形验证码作为二次防护。
- 更换邮箱(/api/v1/user/change-email
- 更换邮箱时,建议增加图形验证码与邮箱验证码双重校验,降低账户被恶意修改的风险。
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
## 依赖关系分析
- 路由层依赖处理器层暴露的处理函数。
- 处理器层依赖服务层提供的生成与验证方法。
- 服务层依赖Redis客户端进行数据持久化与过期控制。
- 通用响应模型用于统一返回格式(尽管验证码端点未直接使用该模型,但整体风格一致)。
```mermaid
graph LR
Routes["routes.go"] --> Handler["captcha_handler.go"]
Handler --> Service["captcha_service.go"]
Service --> Redis["redis.go"]
Handler --> Model["response.go"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L80-L86)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [response.go](file://internal/model/response.go#L1-L86)
章节来源
- [routes.go](file://internal/handler/routes.go#L80-L86)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [response.go](file://internal/model/response.go#L1-L86)
## 性能考虑
- 生成阶段
- 滑块拼图生成与base64编码在单次请求中完成建议在前端缓存主图/滑块图以减少重复生成开销。
- 验证阶段
- Redis读取与删除均为O(1),验证逻辑简单,延迟低。
- 过期策略
- 默认5分钟过期避免长期占用内存过期后自动失效无需手动清理。
- 并发与重复使用
- 验证通过后立即删除Redis键防止同一验证码被重复使用。
章节来源
- [captcha_service.go](file://internal/service/captcha_service.go#L123-L135)
- [captcha_service.go](file://internal/service/captcha_service.go#L157-L166)
## 故障排查指南
- 生成失败
- 现象返回500消息包含“生成验证码失败”。
- 可能原因滑块拼图生成异常、Redis写入失败。
- 排查步骤检查Redis连接状态、确认滑块拼图资源可用。
- 验证失败
- 现象返回500或400消息提示“验证码已过期或无效”或“验证失败请重试”。
- 可能原因captchaId无效或已过期、dx不在容差范围内、Redis读取异常。
- 排查步骤确认captchaId正确、dx传值合理、Redis键存在且未过期。
- 参数错误
- 现象返回400消息提示“参数错误”。
- 可能原因缺少captchaId或dx、JSON格式不正确。
- 排查步骤:检查请求体字段与类型。
章节来源
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L12-L34)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L36-L76)
- [captcha_service.go](file://internal/service/captcha_service.go#L137-L166)
## 结论
验证码API通过滑块拼图与Redis过期机制提供了轻量级、易部署的人机验证方案。其与注册、登录、更换邮箱等关键流程结合可显著提升账户安全。建议在高风险场景引入图形验证码作为附加防护并优化前端缓存策略以提升用户体验。
## 附录
### 请求/响应示例(路径)
- 获取验证码
- 请求GET /api/v1/captcha/generate
- 响应JSON { "code": 200, "data": { "masterImage": "...", "tileImage": "...", "captchaId": "...", "y": 123 } }
- 验证验证码
- 请求POST /api/v1/captcha/verify
- JSON{ "captchaId": "...", "dx": 123 }
- 响应:
- 成功JSON { "code": 200, "msg": "验证成功" }
- 失败JSON { "code": 400, "msg": "验证失败,请重试" }
章节来源
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L12-L34)
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L36-L76)
### 单元测试要点(路径)
- 验证码生成与过期时间
- 测试TestGenerateCaptchaData_ExpireTime
- 断言过期时间为300秒5分钟
- 验证码验证逻辑
- 测试TestVerifyCaptchaData_Logic
- 断言在容差范围内±3返回成功超出范围返回失败
- Redis键生成
- 测试TestVerifyCaptchaData_RedisKey
- 断言键名为“captcha:{id}”
- 处理器响应格式与错误处理
- 测试TestCaptchaHandler_ResponseFormat、TestCaptchaHandler_ErrorHandling
- 断言:成功/失败响应格式与HTTP状态码
章节来源
- [captcha_service_test.go](file://internal/service/captcha_service_test.go#L1-L175)
- [captcha_handler_test.go](file://internal/handler/captcha_handler_test.go#L1-L134)

View File

@@ -1,220 +0,0 @@
# Redis缓存集成
<cite>
**本文档引用的文件**
- [redis.go](file://pkg/redis/redis.go)
- [manager.go](file://pkg/redis/manager.go)
- [config.go](file://pkg/config/config.go)
- [jwt.go](file://pkg/auth/jwt.go)
- [captcha_service.go](file://internal/service/captcha_service.go)
- [verification_service.go](file://internal/service/verification_service.go)
</cite>
## 目录
1. [项目结构](#项目结构)
2. [Redis客户端封装设计](#redis客户端封装设计)
3. [连接池与健康检查机制](#连接池与健康检查机制)
4. [缓存操作实现原理](#缓存操作实现原理)
5. [JWT令牌存储应用](#jwt令牌存储应用)
6. [会话管理与验证码应用](#会话管理与验证码应用)
7. [限流控制实现](#限流控制实现)
8. [缓存问题预防策略](#缓存问题预防策略)
9. [高可用架构支持](#高可用架构支持)
## 项目结构
```mermaid
graph TB
subgraph "pkg"
Redis[redis]
Config[config]
Auth[auth]
end
subgraph "internal"
Service[service]
Repository[repository]
end
Redis --> Service
Config --> Redis
Auth --> Service
Service --> Repository
```
**图示来源**
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [config.go](file://pkg/config/config.go#L1-L305)
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
## Redis客户端封装设计
CarrotSkin项目中的Redis客户端封装采用分层设计模式通过`Client`结构体对`github.com/redis/go-redis/v9`库进行包装提供更简洁的API接口。`Client`结构体包含`*redis.Client`指针和`*zap.Logger`日志记录器,实现了日志记录和错误处理的统一管理。
封装设计遵循单一职责原则将Redis连接管理、操作执行和日志记录分离。通过`New`函数创建客户端实例时,会根据配置参数初始化连接,并执行连接测试,确保连接的可用性。这种设计模式提高了代码的可维护性和可测试性。
**本节来源**
- [redis.go](file://pkg/redis/redis.go#L15-L52)
- [manager.go](file://pkg/redis/manager.go#L11-L18)
## 连接池与健康检查机制
Redis连接池配置在`RedisConfig`结构体中定义,包含`Host``Port``Password``Database``PoolSize`等关键参数。连接池大小通过`PoolSize`字段配置默认值为10可根据实际负载情况进行调整。
健康检查机制在客户端初始化时实现,通过`Ping`命令测试连接可用性。使用`context.WithTimeout`设置5秒超时防止连接测试阻塞主线程。连接成功后会记录包含主机、端口和数据库信息的日志便于监控和故障排查。
```mermaid
sequenceDiagram
participant App as 应用程序
participant Manager as Redis管理器
participant Client as Redis客户端
participant Redis as Redis服务器
App->>Manager : Init(cfg, logger)
Manager->>Client : New(cfg, logger)
Client->>Redis : Ping()
Redis-->>Client : PONG
Client-->>Manager : 返回客户端实例
Manager-->>App : 初始化完成
```
**图示来源**
- [redis.go](file://pkg/redis/redis.go#L22-L47)
- [config.go](file://pkg/config/config.go#L49-L56)
**本节来源**
- [redis.go](file://pkg/redis/redis.go#L22-L47)
- [config.go](file://pkg/config/config.go#L49-L56)
## 缓存操作实现原理
缓存操作通过`Client`结构体的方法实现,包括`Set``Get``Del``Exists``Expire`等基本操作。这些方法直接代理到底层`redis.Client`的对应方法保持了与原生API的一致性。
`Set`方法接受`context.Context`、键、值和过期时间参数,支持设置键值对及其过期时间。`Get`方法返回字符串值和错误,通过`Nil`方法可以判断键不存在的情况。`Expire`方法用于修改现有键的过期时间,支持动态调整缓存策略。
```mermaid
classDiagram
class Client {
+*redis.Client
+*zap.Logger
+Set(ctx, key, value, expiration) error
+Get(ctx, key) (string, error)
+Del(ctx, keys) error
+Exists(ctx, keys) (int64, error)
+Expire(ctx, key, expiration) error
+Nil(err) bool
}
class RedisConfig {
+Host string
+Port int
+Password string
+Database int
+PoolSize int
}
Client --> RedisConfig : 使用
```
**图示来源**
- [redis.go](file://pkg/redis/redis.go#L60-L83)
- [config.go](file://pkg/config/config.go#L49-L56)
**本节来源**
- [redis.go](file://pkg/redis/redis.go#L60-L83)
## JWT令牌存储应用
JWT令牌存储利用Redis作为临时存储介质通过`Set`方法将令牌与用户信息关联存储。令牌的过期时间与JWT的有效期保持一致确保令牌在Redis中的生命周期与JWT一致。
`jwt.go`JWT服务生成令牌后可以通过Redis存储令牌的元数据如用户ID、角色等信息。这种设计实现了无状态认证与有状态缓存的结合既保持了JWT的无状态特性又可以通过Redis快速验证令牌的有效性。
```mermaid
sequenceDiagram
participant User as 用户
participant Auth as 认证服务
participant Redis as Redis缓存
User->>Auth : 登录请求
Auth->>Auth : 生成JWT令牌
Auth->>Redis : Set(token, userInfo, expireTime)
Redis-->>Auth : 存储成功
Auth-->>User : 返回JWT令牌
User->>Auth : 带JWT的请求
Auth->>Redis : Get(token)
Redis-->>Auth : 返回用户信息
Auth-->>User : 处理请求
```
**图示来源**
- [jwt.go](file://pkg/auth/jwt.go#L32-L52)
- [redis.go](file://pkg/redis/redis.go#L60-L63)
**本节来源**
- [jwt.go](file://pkg/auth/jwt.go#L32-L52)
- [redis.go](file://pkg/redis/redis.go#L60-L63)
## 会话管理与验证码应用
会话管理通过Redis存储会话数据每个会话以唯一ID作为键会话数据作为值进行存储。验证码应用在`captcha_service.go`中实现,使用`redisKeyPrefix = "captcha:"`作为键前缀,确保验证码数据的隔离性。
验证码生成时将滑块的目标坐标等验证信息序列化后存储到Redis设置300秒过期时间。验证时从Redis获取原始数据与用户输入进行比对验证成功后立即删除Redis记录防止重复使用。
```mermaid
flowchart TD
Start([生成验证码]) --> Generate["生成滑块坐标(Tx,Ty)"]
Generate --> Serialize["序列化为JSON"]
Serialize --> Store["Set(captcha:id, json, 300s)"]
Store --> Return["返回验证码数据"]
Verify([验证验证码]) --> Get["Get(captcha:id)"]
Get --> Check{"是否存在?"}
Check --> |否| Expired["返回: 验证码已过期"]
Check --> |是| Parse["解析JSON数据"]
Parse --> Validate["验证用户输入"]
Validate --> Delete["Del(captcha:id)"]
Delete --> Result["返回验证结果"]
```
**图示来源**
- [captcha_service.go](file://internal/service/captcha_service.go#L75-L135)
- [redis.go](file://pkg/redis/redis.go#L60-L63)
**本节来源**
- [captcha_service.go](file://internal/service/captcha_service.go#L75-L135)
- [redis.go](file://pkg/redis/redis.go#L144-L146)
## 限流控制实现
限流控制在`verification_service.go`中实现通过Redis的`Set`命令设置频率限制。使用`codeType``email`组合生成限流键,值设为"1",过期时间由`CodeRateLimit`常量定义。
当用户请求发送验证码时先检查限流键是否存在若存在则拒绝请求防止频繁发送。这种基于Redis的限流机制简单高效能够有效防止恶意刷屏攻击保护系统资源。
```mermaid
flowchart LR
Request[发送验证码请求] --> Check["Exists(verification:rate:email)"]
Check --> |存在| Reject[拒绝请求]
Check --> |不存在| Send[发送验证码]
Send --> Set["Set(verification:rate:email, 1, 60s)"]
Set --> Response[返回响应]
```
**图示来源**
- [verification_service.go](file://internal/service/verification_service.go#L58-L67)
- [redis.go](file://pkg/redis/redis.go#L75-L78)
**本节来源**
- [verification_service.go](file://internal/service/verification_service.go#L58-L67)
## 缓存问题预防策略
针对缓存穿透、雪崩、击穿问题CarrotSkin采用多种预防策略。对于缓存穿透使用`Nil`方法识别空值情况,避免频繁查询数据库。对于缓存雪崩,建议在配置中设置随机的过期时间偏移,避免大量缓存同时失效。
缓存击穿问题通过互斥锁或逻辑过期策略解决。当热点数据失效时,只允许一个请求加载数据,其他请求等待并使用旧数据,直到新数据加载完成。这种策略既保证了数据的一致性,又避免了数据库的瞬时压力。
**本节来源**
- [redis.go](file://pkg/redis/redis.go#L160-L162)
- [captcha_service.go](file://internal/service/captcha_service.go#L144-L145)
## 高可用架构支持
Redis高可用架构支持通过配置文件中的连接参数实现。虽然当前代码主要针对单机模式`github.com/redis/go-redis/v9`库原生支持哨兵模式和集群模式。通过修改配置,可以无缝切换到高可用架构。
连接故障恢复机制内置于客户端库中,自动处理网络抖动和临时故障。建议在生产环境中配置合理的超时时间和重试策略,确保系统的稳定性和可靠性。监控连接状态和性能指标,及时发现和解决潜在问题。
**本节来源**
- [redis.go](file://pkg/redis/redis.go#L24-L32)
- [config.go](file://pkg/config/config.go#L49-L56)

View File

@@ -1,314 +0,0 @@
# 外部集成
<cite>
**本文档引用的文件**
- [email.go](file://pkg/email/email.go)
- [manager.go](file://pkg/email/manager.go)
- [minio.go](file://pkg/storage/minio.go)
- [storage_manager.go](file://pkg/storage/manager.go)
- [redis.go](file://pkg/redis/redis.go)
- [redis_manager.go](file://pkg/redis/manager.go)
- [config.go](file://pkg/config/config.go)
- [manager.go](file://pkg/config/manager.go)
- [verification_service.go](file://internal/service/verification_service.go)
- [upload_service.go](file://internal/service/upload_service.go)
</cite>
## 目录
1. [简介](#简介)
2. [邮件服务集成](#邮件服务集成)
3. [对象存储集成](#对象存储集成)
4. [缓存系统集成](#缓存系统集成)
5. [集成初始化流程](#集成初始化流程)
6. [故障排除与性能调优](#故障排除与性能调优)
7. [总结](#总结)
## 简介
CarrotSkin项目通过集成第三方服务来实现关键功能包括邮件服务SMTP、对象存储MinIO/RustFS和缓存系统Redis。这些集成分别用于发送验证邮件、存储用户上传的皮肤文件以及缓存会话数据和验证码。本文档详细说明这些外部服务的配置、初始化和使用方式为初学者提供配置示例同时为经验丰富的开发者提供故障排除和性能调优的高级技巧。
## 邮件服务集成
邮件服务用于向用户发送验证码邮件包括注册验证、密码重置和更换邮箱等场景。该服务通过SMTP协议与邮件服务器通信支持SSL/TLS加密。
### 配置与初始化
邮件服务的配置通过环境变量进行管理主要配置项包括SMTP主机、端口、用户名、密码和发件人名称。服务采用单例模式初始化确保线程安全。
```mermaid
classDiagram
class EmailConfig {
+bool Enabled
+string SMTPHost
+int SMTPPort
+string Username
+string Password
+string FromName
}
class Service {
-EmailConfig cfg
-*zap.Logger logger
+SendVerificationCode(to, code, purpose) error
+SendResetPassword(to, code) error
+SendEmailVerification(to, code) error
+SendChangeEmail(to, code) error
}
class Manager {
+Init(cfg EmailConfig, logger *zap.Logger) error
+GetService() (*Service, error)
+MustGetService() *Service
}
EmailConfig --> Service : "配置"
Service --> Manager : "实现"
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L98-L106)
- [email.go](file://pkg/email/email.go#L16-L19)
- [manager.go](file://pkg/email/manager.go#L11-L18)
**Section sources**
- [config.go](file://pkg/config/config.go#L98-L106)
- [email.go](file://pkg/email/email.go#L16-L19)
- [manager.go](file://pkg/email/manager.go#L11-L18)
### 使用方式
邮件服务提供了多种发送验证码的方法,根据不同的业务场景调用相应的函数。例如,`SendEmailVerification`用于发送邮箱验证邮件,`SendResetPassword`用于发送密码重置邮件。
```mermaid
sequenceDiagram
participant Handler as "Handler"
participant Service as "VerificationService"
participant EmailService as "EmailService"
Handler->>Service : SendVerificationCode()
Service->>Service : GenerateVerificationCode()
Service->>Service : 存储验证码到Redis
Service->>EmailService : SendEmailVerification()
EmailService->>EmailService : 构建邮件内容
EmailService->>SMTP : 发送邮件
SMTP-->>EmailService : 发送结果
EmailService-->>Service : 发送结果
Service-->>Handler : 结果
```
**Diagram sources**
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
- [email.go](file://pkg/email/email.go#L29-L55)
**Section sources**
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
- [email.go](file://pkg/email/email.go#L29-L55)
## 对象存储集成
对象存储服务用于存储用户上传的皮肤文件和头像支持S3兼容的存储系统如MinIO和RustFS。该服务提供预签名URL功能允许客户端直接上传文件到存储服务器。
### 配置与初始化
对象存储的配置包括端点地址、访问密钥、密钥和是否使用SSL。存储桶名称通过环境变量配置支持多个存储桶。服务同样采用单例模式初始化。
```mermaid
classDiagram
class RustFSConfig {
+string Endpoint
+string AccessKey
+string SecretKey
+bool UseSSL
+map[string]string Buckets
}
class StorageClient {
-*minio.Client client
-map[string]string buckets
+GetBucket(name) (string, error)
+GeneratePresignedURL(ctx, bucket, object, expires) (string, error)
+GeneratePresignedPostURL(ctx, bucket, object, minSize, maxSize, expires, useSSL, endpoint) (*PresignedPostPolicyResult, error)
}
class Manager {
+Init(cfg RustFSConfig) error
+GetClient() (*StorageClient, error)
+MustGetClient() *StorageClient
}
RustFSConfig --> StorageClient : "配置"
StorageClient --> Manager : "实现"
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L58-L64)
- [minio.go](file://pkg/storage/minio.go#L15-L18)
- [manager.go](file://pkg/storage/manager.go#L9-L16)
**Section sources**
- [config.go](file://pkg/config/config.go#L58-L64)
- [minio.go](file://pkg/storage/minio.go#L15-L18)
- [manager.go](file://pkg/storage/manager.go#L9-L16)
### 使用方式
对象存储服务通过生成预签名URL来实现文件上传。客户端获取URL后可以直接上传文件无需经过应用服务器。这提高了上传效率并减少了服务器负载。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "Handler"
participant Service as "UploadService"
participant Storage as "StorageClient"
Client->>Handler : 请求上传URL
Handler->>Service : GenerateAvatarUploadURL()
Service->>Storage : GeneratePresignedPostURL()
Storage-->>Service : 预签名URL和表单数据
Service-->>Handler : 上传配置
Handler-->>Client : 上传配置
Client->>Storage : 使用预签名URL上传文件
Storage-->>Client : 上传结果
```
**Diagram sources**
- [upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [minio.go](file://pkg/storage/minio.go#L82-L120)
**Section sources**
- [upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [minio.go](file://pkg/storage/minio.go#L82-L120)
## 缓存系统集成
缓存系统使用Redis来存储临时数据如验证码、会话信息和频率限制。Redis提供了高性能的键值存储支持多种数据结构。
### 配置与初始化
Redis的配置包括主机地址、端口、密码、数据库编号和连接池大小。服务采用单例模式初始化确保线程安全。
```mermaid
classDiagram
class RedisConfig {
+string Host
+int Port
+string Password
+int Database
+int PoolSize
}
class Client {
-*redis.Client Client
-*zap.Logger logger
+Set(ctx, key, value, expiration) error
+Get(ctx, key) (string, error)
+Del(ctx, keys) error
+Exists(ctx, keys) (int64, error)
+Expire(ctx, key, expiration) error
+Incr(ctx, key) (int64, error)
+Decr(ctx, key) (int64, error)
+HSet(ctx, key, values) error
+HGet(ctx, key, field) (string, error)
+HGetAll(ctx, key) (map[string]string, error)
+HDel(ctx, key, fields) error
+SAdd(ctx, key, members) error
+SMembers(ctx, key) ([]string, error)
+SRem(ctx, key, members) error
+SIsMember(ctx, key, member) (bool, error)
+ZAdd(ctx, key, members) error
+ZRange(ctx, key, start, stop) ([]string, error)
+ZRem(ctx, key, members) error
+Pipeline() redis.Pipeliner
+TxPipeline() redis.Pipeliner
+Nil(err) bool
+GetBytes(ctx, key) ([]byte, error)
}
class Manager {
+Init(cfg RedisConfig, logger *zap.Logger) error
+GetClient() (*Client, error)
+MustGetClient() *Client
}
RedisConfig --> Client : "配置"
Client --> Manager : "实现"
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L49-L56)
- [redis.go](file://pkg/redis/redis.go#L16-L19)
- [manager.go](file://pkg/redis/manager.go#L11-L18)
**Section sources**
- [config.go](file://pkg/config/config.go#L49-L56)
- [redis.go](file://pkg/redis/redis.go#L16-L19)
- [manager.go](file://pkg/redis/manager.go#L11-L18)
### 使用方式
缓存系统主要用于存储验证码和频率限制。当用户请求发送验证码时系统会先检查频率限制然后生成验证码并存储到Redis中最后发送邮件。
```mermaid
sequenceDiagram
participant Handler as "Handler"
participant Service as "VerificationService"
participant Redis as "RedisClient"
Handler->>Service : SendVerificationCode()
Service->>Redis : Exists(verification : rate_limit : *)
Redis-->>Service : 结果
alt 频率限制未超
Service->>Service : GenerateVerificationCode()
Service->>Redis : Set(verification : code : *, code, expiration)
Service->>Redis : Set(verification : rate_limit : *, 1, rateLimit)
Service->>EmailService : SendEmailVerification()
EmailService-->>Service : 发送结果
Service-->>Handler : 结果
else 频率限制已超
Service-->>Handler : 错误
end
```
**Diagram sources**
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
- [redis.go](file://pkg/redis/redis.go#L60-L67)
**Section sources**
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
- [redis.go](file://pkg/redis/redis.go#L60-L67)
## 集成初始化流程
所有外部集成服务的初始化都在应用启动时完成,通过配置管理器加载配置并初始化各个服务。初始化流程确保了服务的线程安全和单例模式。
```mermaid
flowchart TD
Start([应用启动]) --> LoadConfig["加载配置 (config.Load)"]
LoadConfig --> InitEmail["初始化邮件服务 (email.Init)"]
LoadConfig --> InitStorage["初始化存储服务 (storage.Init)"]
LoadConfig --> InitRedis["初始化Redis服务 (redis.Init)"]
InitEmail --> CheckEmail["检查邮件服务是否启用"]
InitStorage --> TestStorage["测试存储连接"]
InitRedis --> TestRedis["测试Redis连接"]
CheckEmail --> End1([邮件服务就绪])
TestStorage --> End2([存储服务就绪])
TestRedis --> End3([Redis服务就绪])
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L109-L133)
- [manager.go](file://pkg/email/manager.go#L20-L26)
- [manager.go](file://pkg/storage/manager.go#L19-L26)
- [manager.go](file://pkg/redis/manager.go#L21-L28)
**Section sources**
- [config.go](file://pkg/config/config.go#L109-L133)
- [manager.go](file://pkg/email/manager.go#L20-L26)
- [manager.go](file://pkg/storage/manager.go#L19-L26)
- [manager.go](file://pkg/redis/manager.go#L21-L28)
## 故障排除与性能调优
### 邮件服务
- **常见问题**SMTP连接失败、认证失败、邮件发送超时。
- **解决方案**检查SMTP主机和端口配置确保SSL/TLS设置正确验证用户名和密码。
- **性能调优**:使用连接池减少连接开销,批量发送邮件以减少网络延迟。
### 对象存储
- **常见问题**预签名URL无效、上传失败、存储桶不存在。
- **解决方案**检查存储桶名称和权限确保预签名URL的过期时间合理验证访问密钥和密钥。
- **性能调优**使用分块上传大文件启用CDN加速文件访问。
### 缓存系统
- **常见问题**Redis连接失败、内存不足、键过期。
- **解决方案**检查Redis主机和端口配置优化键的过期时间监控内存使用情况。
- **性能调优**:使用管道减少网络往返,合理设置连接池大小,定期清理过期键。
**Section sources**
- [email.go](file://pkg/email/email.go#L66-L86)
- [minio.go](file://pkg/storage/minio.go#L33-L42)
- [redis.go](file://pkg/redis/redis.go#L34-L40)
## 总结
CarrotSkin项目通过集成邮件服务、对象存储和缓存系统实现了高效、可靠的用户验证和文件存储功能。这些集成通过统一的配置管理和初始化流程确保了服务的稳定性和可维护性。开发者可以根据本文档的指导轻松配置和使用这些外部服务并通过故障排除和性能调优技巧进一步提升系统的性能和可靠性。

View File

@@ -1,345 +0,0 @@
# 对象存储集成
<cite>
**本文引用的文件**
- [minio.go](file://pkg/storage/minio.go)
- [manager.go](file://pkg/storage/manager.go)
- [config.go](file://pkg/config/config.go)
- [upload_service.go](file://internal/service/upload_service.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [texture_service.go](file://internal/service/texture_service.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向CarrotSkin后端的对象存储集成聚焦于如何通过MinIO客户端与S3兼容存储系统交互。文档围绕以下目标展开
- 解析minio.go中初始化客户端的流程包括访问密钥、端点配置和TLS设置
- 说明manager.go中Upload、Download、GeneratePresignedURL等关键方法的实现细节与调用方式
- 提供皮肤/披风文件上传、私有资源临时链接生成等典型用例的代码示例路径
- 解释分片上传、断点续传等高级功能的支持情况
- 为运维人员提供性能调优建议(并发控制、连接复用)和故障排查指南(签名错误、网络超时)
## 项目结构
对象存储相关代码集中在pkg/storage目录配置在pkg/config业务侧在internal/service与internal/handler中调用。
```mermaid
graph TB
subgraph "配置层"
CFG["pkg/config/config.go<br/>RustFSConfig"]
end
subgraph "存储客户端"
MINIO["pkg/storage/minio.go<br/>StorageClient<br/>NewStorage/GeneratePresignedURL/GeneratePresignedPostURL"]
MGR["pkg/storage/manager.go<br/>Init/GetClient/MustGetClient"]
end
subgraph "业务服务"
SVC_UPLOAD["internal/service/upload_service.go<br/>GenerateAvatarUploadURL/GenerateTextureUploadURL"]
SVC_TEX["internal/service/texture_service.go<br/>CreateTexture/RecordTextureDownload 等"]
end
subgraph "接口层"
HANDLER_TEX["internal/handler/texture_handler.go<br/>GenerateTextureUploadURL 接口"]
end
CFG --> MINIO
MINIO --> MGR
MGR --> HANDLER_TEX
HANDLER_TEX --> SVC_UPLOAD
SVC_UPLOAD --> MINIO
SVC_TEX --> MINIO
```
图表来源
- [minio.go](file://pkg/storage/minio.go#L1-L120)
- [manager.go](file://pkg/storage/manager.go#L1-L49)
- [config.go](file://pkg/config/config.go#L58-L66)
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
章节来源
- [minio.go](file://pkg/storage/minio.go#L1-L120)
- [manager.go](file://pkg/storage/manager.go#L1-L49)
- [config.go](file://pkg/config/config.go#L58-L66)
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
## 核心组件
- StorageClient封装minio-go客户端提供桶名解析、预签名URL生成等能力
- manager提供全局单例的存储客户端初始化与获取
- RustFSConfig承载S3兼容存储的端点、凭据与桶映射
- upload_service面向业务的上传URL生成工具按头像与材质类型分别组织对象路径
- texture_handler对外暴露生成上传URL的HTTP接口
- texture_service材质实体的增删改查与下载计数等业务逻辑
章节来源
- [minio.go](file://pkg/storage/minio.go#L14-L120)
- [manager.go](file://pkg/storage/manager.go#L9-L44)
- [config.go](file://pkg/config/config.go#L58-L66)
- [upload_service.go](file://internal/service/upload_service.go#L13-L57)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
## 架构总览
下图展示从接口到存储的调用链路与职责划分。
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "TextureHandler"
participant S as "UploadService"
participant M as "StorageManager"
participant SC as "StorageClient"
participant O as "对象存储(兼容S3)"
C->>H : "POST /api/v1/texture/upload-url"
H->>M : "MustGetClient()"
M-->>H : "*StorageClient"
H->>S : "GenerateTextureUploadURL(ctx, client, cfg, userID, fileName, type)"
S->>SC : "GetBucket(\"textures\")"
S->>SC : "GeneratePresignedPostURL(bucket, objectName, limits, expires, useSSL, endpoint)"
SC->>O : "PresignedPostPolicy(policy)"
O-->>SC : "postURL + formData"
SC-->>S : "PresignedPostPolicyResult"
S-->>H : "PresignedPostPolicyResult"
H-->>C : "返回postURL、formData、文件最终URL、过期秒数"
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [manager.go](file://pkg/storage/manager.go#L30-L44)
- [minio.go](file://pkg/storage/minio.go#L57-L120)
## 详细组件分析
### StorageClient与初始化流程minio.go
- 客户端初始化
- 通过端点、凭据与TLS标志创建minio-go客户端
- 若提供了访问密钥与密钥,则进行连接测试(带超时)
- 绑定桶映射,便于按“类型”解析真实桶名
- 关键方法
- GetClient返回底层minio.Client
- GetBucket按名称解析桶名不存在时报错
- GeneratePresignedURL生成预签名PUT URL支持上传
- GeneratePresignedPostURL生成预签名POST策略URL支持表单直传并构造最终访问URL
```mermaid
classDiagram
class StorageClient {
-client : "minio.Client"
-buckets : "map[string]string"
+GetClient() "*minio.Client"
+GetBucket(name) "(string, error)"
+GeneratePresignedURL(ctx, bucketName, objectName, expires) "(string, error)"
+GeneratePresignedPostURL(ctx, bucketName, objectName, minSize, maxSize, expires, useSSL, endpoint) "(*PresignedPostPolicyResult, error)"
}
class PresignedPostPolicyResult {
+PostURL : "string"
+FormData : "map[string]string"
+FileURL : "string"
}
StorageClient --> PresignedPostPolicyResult : "返回"
```
图表来源
- [minio.go](file://pkg/storage/minio.go#L14-L120)
章节来源
- [minio.go](file://pkg/storage/minio.go#L20-L49)
- [minio.go](file://pkg/storage/minio.go#L52-L64)
- [minio.go](file://pkg/storage/minio.go#L66-L73)
- [minio.go](file://pkg/storage/minio.go#L82-L120)
### 存储客户端管理器manager.go
- Init(cfg):线程安全初始化,仅执行一次
- GetClient():获取全局实例,未初始化时报错
- MustGetClient()获取全局实例未初始化时panic
```mermaid
flowchart TD
Start(["调用 Init(cfg)"]) --> Once["sync.Once 保证只执行一次"]
Once --> NewStorage["NewStorage(cfg) 创建 StorageClient"]
NewStorage --> Bind["绑定 clientInstance"]
Bind --> Done(["初始化完成"])
GetClient["GetClient()"] --> Check{"clientInstance 是否存在?"}
Check --> |否| Err["返回错误:未初始化"]
Check --> |是| Return["返回 clientInstance"]
MustGetClient["MustGetClient()"] --> GetClient
GetClient --> Panic{"err 是否非空?"}
Panic --> |是| PanicCall["panic(err)"]
Panic --> |否| Return
```
图表来源
- [manager.go](file://pkg/storage/manager.go#L9-L44)
章节来源
- [manager.go](file://pkg/storage/manager.go#L18-L27)
- [manager.go](file://pkg/storage/manager.go#L29-L35)
- [manager.go](file://pkg/storage/manager.go#L37-L44)
### 配置结构config.go
- RustFSConfig包含端点、访问密钥、密钥、TLS开关与桶映射
- 环境变量映射与默认值设置,支持通过环境变量覆盖
章节来源
- [config.go](file://pkg/config/config.go#L58-L66)
- [config.go](file://pkg/config/config.go#L190-L236)
- [config.go](file://pkg/config/config.go#L238-L305)
### 上传服务与典型用例upload_service.go
- 文件类型与上传配置
- FileTypeAvatar/FileTypeTexture两类
- 各自的允许扩展名、最小/最大尺寸、过期时间
- 生成上传URL
- GenerateAvatarUploadURL按用户ID与时间戳生成对象路径调用StorageClient.GeneratePresignedPostURL
- GenerateTextureUploadURL支持SKIN/CAPE两种材质类型生成对应路径调用StorageClient.GeneratePresignedPostURL
- 下载与公开资源
- 当前代码未直接暴露Download方法公开资源可通过最终访问URL直接访问
- 私有资源可通过GeneratePresignedURL生成临时下载链接
```mermaid
sequenceDiagram
participant Svc as "UploadService"
participant SC as "StorageClient"
participant O as "对象存储"
Svc->>Svc : "ValidateFileName(fileName, type)"
Svc->>SC : "GetBucket(\"avatars\" 或 \"textures\")"
Svc->>SC : "GeneratePresignedPostURL(bucket, objectName, minSize, maxSize, expires, useSSL, endpoint)"
SC->>O : "PresignedPostPolicy(policy)"
O-->>SC : "postURL + formData"
SC-->>Svc : "PresignedPostPolicyResult"
Svc-->>Svc : "返回结果"
```
图表来源
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
- [minio.go](file://pkg/storage/minio.go#L82-L120)
章节来源
- [upload_service.go](file://internal/service/upload_service.go#L13-L57)
- [upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
### 接口层调用texture_handler.go
- GenerateTextureUploadURL接口接收请求体调用UploadService生成预签名POST URL返回postURL、formData与最终文件URL
- CreateTexture接口文件上传完成后创建材质记录到数据库
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
### 材质服务texture_service.go
- CreateTexture校验用户存在、去重校验哈希、转换材质类型、创建记录
- RecordTextureDownload增加下载计数并记录日志
- 其他查询与权限控制方法
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [texture_service.go](file://internal/service/texture_service.go#L162-L187)
## 依赖关系分析
- 配置到客户端RustFSConfig -> NewStorage -> StorageClient
- 客户端到管理器StorageClient -> ManagerInit/GetClient/MustGetClient
- 业务到客户端UploadService/TextureService -> StorageClient
- 接口到业务TextureHandler -> UploadService -> StorageClient
```mermaid
graph LR
CFG["RustFSConfig"] --> NS["NewStorage(cfg)"]
NS --> SC["StorageClient"]
SC --> M["Manager.Init/GetClient/MustGetClient"]
M --> H["TextureHandler"]
H --> US["UploadService"]
US --> SC
TS["TextureService"] --> SC
```
图表来源
- [config.go](file://pkg/config/config.go#L58-L66)
- [minio.go](file://pkg/storage/minio.go#L20-L49)
- [manager.go](file://pkg/storage/manager.go#L18-L44)
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
## 性能考虑
- 并发控制
- 上传直传采用预签名POST策略客户端直接向对象存储发起请求避免服务端转发带来的CPU与内存压力
- 服务端仅负责生成策略与返回表单数据,适合高并发场景
- 连接复用
- minio-go客户端内部维护连接池与HTTP复用建议在生产环境中保持长连接避免频繁重建
- 超时与重试
- 初始化阶段对ListBuckets设置了超时防止阻塞启动
- 业务侧建议在调用方为上传/下载操作设置合理超时与指数退避重试
- 缓存与预热
- 对频繁使用的桶名解析与策略生成可做缓存(需注意策略过期时间)
- 资源限制
- 通过上传配置限制文件大小与扩展名,降低存储与带宽压力
[本节为通用指导,无需特定文件引用]
## 故障排查指南
- 签名错误
- 现象:表单上传返回签名错误
- 排查要点确认formData中多余字段被移除确保file字段位于表单末尾检查endpoint与useSSL一致性
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L82-L120)
- 网络超时
- 现象初始化或ListBuckets超时
- 排查要点检查endpoint连通性、防火墙、TLS证书适当增大超时时间
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L33-L42)
- 凭据错误
- 现象:连接测试失败或策略生成失败
- 排查要点核对AccessKey/SecretKey确认桶映射正确检查对象存储端策略与权限
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L20-L49)
- 桶不存在
- 现象GetBucket返回错误
- 排查要点确认RustFSConfig.Buckets中包含所需桶名核对环境变量覆盖逻辑
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L57-L64)[config.go](file://pkg/config/config.go#L238-L253)
- 接口调用失败
- 现象接口返回400/500
- 排查要点检查鉴权中间件、请求体绑定、日志输出确认MustGetClient/MustGetRustFSConfig已调用
- 参考实现位置:[texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)[manager.go](file://pkg/storage/manager.go#L37-L44)[config.go](file://pkg/config/config.go#L190-L236)
章节来源
- [minio.go](file://pkg/storage/minio.go#L20-L49)
- [minio.go](file://pkg/storage/minio.go#L57-L64)
- [minio.go](file://pkg/storage/minio.go#L82-L120)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [manager.go](file://pkg/storage/manager.go#L37-L44)
- [config.go](file://pkg/config/config.go#L238-L253)
## 结论
- CarrotSkin通过StorageClient与minio-go实现了对S3兼容存储的完整封装重点支持预签名POST直传与PUT上传
- 上传流程清晰:接口层生成策略,前端直传对象存储,服务端仅负责策略与元数据登记
- 分片上传与断点续传当前未在代码中直接体现;如需支持,可在客户端侧采用分片上传策略,并在服务端补充断点续传与合并逻辑
- 建议结合业务需求完善下载与公开/私有资源的访问策略,并持续优化并发与连接复用
[本节为总结性内容,无需特定文件引用]
## 附录
### 典型用例示例(代码示例路径)
- 皮肤/披风文件上传
- 生成预签名POST URL[upload_service.go](file://internal/service/upload_service.go#L117-L160)
- 接口调用入口:[texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- 私有资源临时链接生成PUT
- 生成预签名PUT URL[minio.go](file://pkg/storage/minio.go#L66-L73)
- 业务侧调用:[upload_service.go](file://internal/service/upload_service.go#L78-L115)
### 高级功能支持现状
- 分片上传/断点续传
- 当前未在代码中直接实现;如需支持,可在客户端侧引入分片上传策略,并在服务端补充断点续传与合并逻辑
- 下载
- 当前未直接暴露Download方法可通过最终访问URL或GeneratePresignedURL生成临时下载链接
[本节为概念性说明,无需特定文件引用]

View File

@@ -1,361 +0,0 @@
# 邮件服务集成
<cite>
**本文引用的文件列表**
- [email.go](file://pkg/email/email.go)
- [manager.go](file://pkg/email/manager.go)
- [config.go](file://pkg/config/config.go)
- [verification_service.go](file://internal/service/verification_service.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [redis.go](file://pkg/redis/redis.go)
- [manager.go](file://pkg/redis/manager.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能与可靠性](#性能与可靠性)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向CarrotSkin项目的开发者与运维人员系统性介绍基于SMTP的邮件服务实现与集成方式。重点覆盖以下内容
- 邮件服务核心结构体与方法如Service、SendVerificationCode等的实现逻辑
- SMTP服务器参数配置主机、端口、认证信息与客户端初始化流程
- 注册验证、密码重置、更换邮箱等场景的邮件发送流程
- 与用户认证流程的集成方式(验证码生成、有效期管理、频率限制与安全考虑)
- 初学者本地测试配置建议如使用MailHog
- 高级用户可扩展的方向(错误重试机制、连接池优化、监控指标)
## 项目结构
邮件服务位于独立的pkg模块中并与配置、Redis缓存、业务服务层以及HTTP处理器协同工作。整体组织如下
- 配置层集中定义EmailConfig并通过环境变量加载
- 邮件服务层封装SMTP发送、主题与HTML正文构建
- 缓存层使用Redis存储验证码与频率限制
- 业务服务层:生成验证码、校验验证码、根据类型路由到具体邮件方法
- HTTP处理器对外暴露发送验证码接口调用业务服务层
```mermaid
graph TB
subgraph "配置层"
CFG["EmailConfig<br/>环境变量绑定"]
end
subgraph "邮件服务层"
SVC["Service<br/>send()/Send* 方法"]
MGR["manager.go<br/>Init/GetService/MustGetService"]
end
subgraph "缓存层"
RCL["Redis Client<br/>Set/Get/Del/Exists"]
end
subgraph "业务服务层"
VSRV["verification_service.go<br/>SendVerificationCode/VerifyCode"]
end
subgraph "HTTP处理器"
AH["auth_handler.go<br/>SendVerificationCode 接口"]
end
CFG --> MGR
MGR --> SVC
SVC --> RCL
AH --> VSRV
VSRV --> SVC
VSRV --> RCL
```
图表来源
- [email.go](file://pkg/email/email.go#L1-L163)
- [manager.go](file://pkg/email/manager.go#L1-L48)
- [config.go](file://pkg/config/config.go#L98-L107)
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
章节来源
- [email.go](file://pkg/email/email.go#L1-L163)
- [manager.go](file://pkg/email/manager.go#L1-L48)
- [config.go](file://pkg/config/config.go#L98-L107)
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
## 核心组件
- Service邮件服务
- 职责封装SMTP发送、主题与HTML正文构建、TLS/STARTTLS选择、日志记录
- 关键方法SendVerificationCode、SendResetPassword、SendEmailVerification、SendChangeEmail、send
- manager邮件服务管理器
- 职责:全局单例初始化、线程安全获取实例、未初始化保护
- 关键方法Init、GetService、MustGetService
- 配置EmailConfig
- 字段Enabled、SMTPHost、SMTPPort、Username、Password、FromName
- 来源:环境变量绑定与默认值设置
- 验证码服务verification_service
- 职责生成6位数字验证码、存储到Redis、设置频率限制、按类型路由到具体邮件方法
- Redis客户端redis
- 职责连接管理、基础KV与集合操作、连接健康检查
- HTTP处理器auth_handler
- 职责:对外暴露发送验证码接口,调用业务服务层
章节来源
- [email.go](file://pkg/email/email.go#L1-L163)
- [manager.go](file://pkg/email/manager.go#L1-L48)
- [config.go](file://pkg/config/config.go#L98-L107)
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
## 架构总览
下图展示从HTTP请求到邮件发送的完整链路包括验证码生成、存储、频率限制与邮件发送。
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "auth_handler.SendVerificationCode"
participant VS as "verification_service.SendVerificationCode"
participant RS as "Redis Client"
participant ES as "email.Service"
participant SMTP as "SMTP服务器"
C->>H : "POST /api/v1/auth/send-code"
H->>VS : "调用发送验证码(类型, 邮箱)"
VS->>RS : "检查频率限制(键 : rate_limit)"
RS-->>VS : "存在/不存在"
VS->>VS : "生成6位数字验证码"
VS->>RS : "写入验证码(键 : code)"
VS->>RS : "设置频率限制(键 : rate_limit)"
VS->>ES : "根据类型调用具体发送方法"
ES->>SMTP : "SMTP发送(465隐式TLS/587 STARTTLS)"
SMTP-->>ES : "结果"
ES-->>VS : "结果"
VS-->>H : "结果"
H-->>C : "发送成功响应"
```
图表来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [email.go](file://pkg/email/email.go#L57-L105)
- [redis.go](file://pkg/redis/redis.go#L60-L78)
## 详细组件分析
### Service邮件服务类图
Service负责SMTP发送、主题与HTML正文构建、TLS/STARTTLS选择与日志记录。
```mermaid
classDiagram
class Service {
-cfg : EmailConfig
-logger : Logger
+SendVerificationCode(to, code, purpose) error
+SendResetPassword(to, code) error
+SendEmailVerification(to, code) error
+SendChangeEmail(to, code) error
-send(to, subject, body) error
-getSubject(purpose) string
-getBody(code, purpose) string
}
```
图表来源
- [email.go](file://pkg/email/email.go#L1-L163)
章节来源
- [email.go](file://pkg/email/email.go#L1-L163)
### 发送流程与TLS策略
- 主题与正文
- 主题根据用途动态选择(邮箱验证、重置密码、更换邮箱)
- 正文为HTML模板包含验证码展示与有效期提示
- SMTP连接策略
- 465端口使用隐式TLSSendWithTLS
- 587端口使用STARTTLSSend
- 认证方式
- 使用PlainAuth进行SMTP认证
- 错误处理
- 失败时记录详细上下文收件人、主题、SMTP主机与端口
- 成功时记录成功日志
```mermaid
flowchart TD
Start(["进入 send 方法"]) --> Build["构建邮件对象<br/>From/To/Subject/HTML/Headers"]
Build --> Auth["构造SMTP认证信息"]
Auth --> Addr["拼接地址 host:port"]
Addr --> PortCheck{"端口为465?"}
PortCheck --> |是| TLS["配置TLS(隐式)<br/>SendWithTLS"]
PortCheck --> |否| StartTLS["Send(显式STARTTLS)"]
TLS --> Result{"发送成功?"}
StartTLS --> Result
Result --> |否| LogErr["记录错误日志并返回错误"]
Result --> |是| LogOK["记录成功日志并返回nil"]
```
图表来源
- [email.go](file://pkg/email/email.go#L57-L105)
章节来源
- [email.go](file://pkg/email/email.go#L57-L105)
### 验证码生成与存储verification_service
- 验证码生成
- 6位纯数字随机验证码
- 存储与有效期
- Redis键命名规范verification:code:{type}:{email}有效期10分钟
- 频率限制
- Redis键verification:rate_limit:{type}:{email}限制1分钟内仅允许一次
- 类型路由
- register -> SendEmailVerification
- reset_password -> SendResetPassword
- change_email -> SendChangeEmail
- 其他 -> SendVerificationCode
```mermaid
flowchart TD
Enter(["进入 SendVerificationCode"]) --> RL["检查频率限制键是否存在"]
RL --> |存在| RateErr["返回发送过于频繁错误"]
RL --> |不存在| Gen["生成6位验证码"]
Gen --> Store["写入Redis: code键 + 过期10分钟"]
Store --> Limit["写入Redis: rate_limit键 + 过期1分钟"]
Limit --> Route{"根据类型路由"}
Route --> |register| SendEV["SendEmailVerification"]
Route --> |reset_password| SendRP["SendResetPassword"]
Route --> |change_email| SendCE["SendChangeEmail"]
Route --> |其他| SendVC["SendVerificationCode"]
SendEV --> Done(["返回成功"])
SendRP --> Done
SendCE --> Done
SendVC --> Done
```
图表来源
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
章节来源
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
### HTTP处理器集成auth_handler
- 发送验证码接口
- 解析请求、调用业务服务层、记录日志、返回响应
- 注册流程中的验证码校验
- 在注册接口中调用VerifyCode校验验证码失败即拒绝注册
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
## 依赖关系分析
- 组件耦合
- Service依赖EmailConfig与Zap日志
- verification_service依赖Redis Client与email.Service
- auth_handler依赖verification_service与日志、Redis
- 外部依赖
- SMTP客户端库用于发送邮件
- Redis客户端库用于KV与限流
- 可能的循环依赖
- 当前结构清晰,未见循环依赖迹象
```mermaid
graph LR
CFG["EmailConfig"] --> MGR["manager.Init/GetService"]
MGR --> SVC["Service"]
SVC --> SMTP["SMTP服务器"]
VS["verification_service"] --> SVC
VS --> RCL["Redis Client"]
AH["auth_handler"] --> VS
```
图表来源
- [email.go](file://pkg/email/email.go#L1-L163)
- [manager.go](file://pkg/email/manager.go#L1-L48)
- [config.go](file://pkg/config/config.go#L98-L107)
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
章节来源
- [email.go](file://pkg/email/email.go#L1-L163)
- [manager.go](file://pkg/email/manager.go#L1-L48)
- [config.go](file://pkg/config/config.go#L98-L107)
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
## 性能与可靠性
- 连接池与并发
- Redis客户端内置连接池PoolSize建议结合业务规模调整
- 邮件发送为短连接,建议避免在同一请求中频繁发送
- 错误重试
- 当前实现未内置自动重试;可在业务层增加指数退避重试策略
- 监控指标
- 建议采集邮件发送成功率、耗时、失败原因分布、Redis读写延迟
- 安全与合规
- 生产环境TLS校验建议开启InsecureSkipVerify=false
- 密码等敏感信息仅在内存中传输,避免落盘
[本节为通用建议,无需列出章节来源]
## 故障排查指南
- 常见问题定位
- 邮件服务未启用检查EmailConfig.Enabled与环境变量
- SMTP认证失败核对Username/Password与SMTPHost/SMTPPort
- TLS握手失败确认端口与TLS策略匹配465隐式TLS vs 587 STARTTLS
- Redis连接失败检查Redis配置与网络连通性
- 日志与告警
- 发送失败会记录详细上下文,优先查看日志中的错误堆栈
- 建议在网关或进程外添加告警规则(如连续失败阈值)
- 本地测试
- 使用MailHog作为本地SMTP代理便于调试与演示
章节来源
- [email.go](file://pkg/email/email.go#L57-L105)
- [config.go](file://pkg/config/config.go#L229-L236)
- [redis.go](file://pkg/redis/redis.go#L22-L52)
## 结论
CarrotSkin的邮件服务以简洁、可测试的方式实现了基于SMTP的验证码邮件发送配合Redis完成验证码生成、存储与频率控制。通过HTTP处理器与业务服务层的清晰分层系统具备良好的可维护性与扩展性。建议在生产环境中完善错误重试、连接池优化与监控指标以进一步提升稳定性与可观测性。
[本节为总结性内容,无需列出章节来源]
## 附录
### SMTP配置与初始化
- 配置项EmailConfig
- Enabled是否启用邮件功能
- SMTPHostSMTP服务器主机
- SMTPPortSMTP服务器端口常用465或587
- Username/PasswordSMTP认证凭据
- FromName发件人显示名称
- 环境变量绑定
- EMAIL_ENABLED、EMAIL_SMTP_HOST、EMAIL_SMTP_PORT、EMAIL_USERNAME、EMAIL_PASSWORD、EMAIL_FROM_NAME
- 初始化步骤
- 通过manager.Init传入EmailConfig与Logger随后通过MustGetService获取实例
- 在应用启动阶段完成初始化,确保后续调用可用
章节来源
- [config.go](file://pkg/config/config.go#L98-L107)
- [config.go](file://pkg/config/config.go#L229-L236)
- [manager.go](file://pkg/email/manager.go#L20-L43)
### 场景化使用路径
- 注册验证
- HTTP接口auth_handler.SendVerificationCode
- 业务流程verification_service.SendVerificationCode -> email.Service.SendEmailVerification
- 密码重置
- 业务流程verification_service.SendVerificationCode -> email.Service.SendResetPassword
- 更换邮箱
- 业务流程verification_service.SendVerificationCode -> email.Service.SendChangeEmail
章节来源
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
- [email.go](file://pkg/email/email.go#L42-L55)
### 本地测试建议MailHog
- 启动MailHog作为本地SMTP代理
- 将EmailConfig配置指向MailHog的SMTP地址与端口
- 通过auth_handler的发送验证码接口验证端到端流程
- 在MailHog Web界面查看邮件内容与收件人信息
[本节为通用建议,无需列出章节来源]

View File

@@ -1,325 +0,0 @@
# 快速开始
<cite>
**本文引用的文件**
- [go.mod](file://go.mod)
- [cmd/server/main.go](file://cmd/server/main.go)
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/config/manager.go](file://pkg/config/manager.go)
- [pkg/database/manager.go](file://pkg/database/manager.go)
- [pkg/storage/manager.go](file://pkg/storage/manager.go)
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
- [scripts/dev.sh](file://scripts/dev.sh)
- [run.sh](file://run.sh)
- [run.bat](file://run.bat)
- [scripts/check-env.sh](file://scripts/check-env.sh)
- [configs/casbin/rbac_model.conf](file://configs/casbin/rbac_model.conf)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能与运行建议](#性能与运行建议)
8. [故障排除指南](#故障排除指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本指南面向首次接触 CarrotSkin 后端项目的开发者,帮助你从零开始完成开发环境准备、配置与依赖安装,并通过提供的脚本快速启动服务。你将学会:
- 准备 Go、数据库与对象存储等运行所需环境
- 复制并配置环境变量与配置文件
- 安装 Swagger 工具并生成 API 文档
- 使用 dev.sh、run.sh 和 run.bat 脚本启动项目
- 面向初学者的路径指引与面向资深用户的常见问题排查
## 项目结构
CarrotSkin 后端采用模块化分层设计,主要目录与职责如下:
- cmd/server应用入口负责初始化配置、数据库、中间件、路由与 HTTP 服务
- pkg/*通用能力封装如配置、数据库、Redis、存储、日志、邮件等
- internal/*:业务层实现,包括处理器、中间件、模型、仓库、服务与类型定义
- scripts开发与环境检查脚本
- configs静态配置与权限模型文件
```mermaid
graph TB
A["cmd/server/main.go<br/>应用入口"] --> B["pkg/config/*<br/>配置加载与管理"]
A --> C["pkg/database/*<br/>数据库初始化与迁移"]
A --> D["pkg/redis/*<br/>Redis连接"]
A --> E["pkg/storage/*<br/>对象存储(RustFS)"]
A --> F["pkg/email/*<br/>邮件服务"]
A --> G["internal/handler/*<br/>路由与Swagger"]
A --> H["internal/middleware/*<br/>中间件"]
A --> I["internal/service/*<br/>业务服务"]
A --> J["internal/repository/*<br/>数据访问层"]
A --> K["internal/model/*<br/>数据模型"]
L["scripts/*<br/>启动与检查脚本"] --> A
M["configs/casbin/*<br/>权限模型"] --> I
```
图表来源
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
- [scripts/dev.sh](file://scripts/dev.sh#L1-L29)
- [configs/casbin/rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
章节来源
- [go.mod](file://go.mod#L1-L92)
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
## 核心组件
- 配置系统:支持从 .env 与环境变量加载,提供默认值与覆盖逻辑,便于本地与生产环境切换
- 数据库:基于 GORM 的 PostgreSQL 连接与自动迁移
- 对象存储RustFSS3 兼容),用于材质与头像等资源存储
- 中间件日志、恢复、CORS
- Swagger自动生成 API 文档并通过路由暴露
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L68)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
## 架构总览
下图展示了应用启动时的关键流程:加载配置 → 初始化日志 → 初始化数据库并迁移 → 初始化 JWT/Redis/存储/邮件 → 注册路由 → 启动 HTTP 服务。
```mermaid
sequenceDiagram
participant CLI as "命令行"
participant Main as "main.go"
participant Cfg as "配置模块"
participant Log as "日志模块"
participant DB as "数据库模块"
participant JWT as "JWT服务"
participant Rds as "Redis"
participant Store as "对象存储"
participant Mail as "邮件服务"
participant Router as "路由与中间件"
participant HTTP as "HTTP服务器"
CLI->>Main : "go run cmd/server/main.go"
Main->>Cfg : "Init()"
Cfg-->>Main : "配置加载完成"
Main->>Log : "Init(日志配置)"
Main->>DB : "Init(数据库配置)"
DB-->>Main : "连接成功"
Main->>DB : "AutoMigrate()"
DB-->>Main : "迁移完成"
Main->>JWT : "Init(JWT配置)"
Main->>Rds : "Init(Redis配置)"
Main->>Store : "Init(RustFS配置)"
Store-->>Main : "连接成功/告警"
Main->>Mail : "Init(邮件配置)"
Main->>Router : "注册中间件与路由"
Main->>HTTP : "ListenAndServe()"
HTTP-->>CLI : "服务就绪"
```
图表来源
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L68)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
## 详细组件分析
### 配置系统与环境变量
- 配置来源优先级:.env 文件 → 环境变量 → 默认值
- 支持的环境变量前缀CARROTSKIN例如 CARROTSKIN_DATABASE_HOST
- 关键配置项部分服务器端口、模式、读写超时数据库主机、端口、用户名、密码、库名、SSL、时区、连接池Redis 主机、端口、密码、库号、池大小RustFS Endpoint、AccessKey、SecretKey、UseSSL、存储桶映射JWT Secret、过期小时日志级别、格式、输出、轮转参数上传最大尺寸、类型限制邮件 SMTP Host/Port/用户名/密码/发送者名称
- 特殊覆盖逻辑可通过独立环境变量覆盖连接池、Redis 池大小、上传限制、邮件开关等
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L68)
### 数据库初始化与迁移
- 初始化:根据配置建立 PostgreSQL 连接
- 迁移按顺序创建用户、档案、材质、认证、Yggdrasil、系统配置、审计日志、权限规则等表
- 关闭:优雅关闭 SQL 连接
章节来源
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
### 对象存储RustFS/S3 兼容)
- 初始化:根据配置连接到 S3 兼容存储
- 行为:连接失败仅记录警告,不影响服务启动(部分功能受限)
章节来源
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
### Swagger 文档
- 路由:/swagger/*any
- 健康检查:/health
- 文档生成:通过 swag 工具扫描注释生成
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
### 权限模型Casbin
- RBAC 模型文件位于 configs/casbin/rbac_model.conf
章节来源
- [configs/casbin/rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
## 依赖关系分析
- 应用入口依赖配置、数据库、Redis、存储、邮件、日志与路由模块
- 配置模块依赖 Viper 与 godotenv
- 数据库模块依赖 GORM 与 PostgreSQL 驱动
- 存储模块依赖 MinIO SDK
- Swagger 依赖 Gin-Swagger 与 Swag 工具
- Redis 依赖官方 Redis 客户端
```mermaid
graph LR
Main["cmd/server/main.go"] --> Cfg["pkg/config/*"]
Main --> DB["pkg/database/*"]
Main --> Rds["pkg/redis/*"]
Main --> Stor["pkg/storage/*"]
Main --> Mail["pkg/email/*"]
Main --> Log["pkg/logger/*"]
Main --> Hdl["internal/handler/*"]
Cfg --> Viper["spf13/viper"]
Cfg --> DotEnv["joho/godotenv"]
DB --> GORM["gorm.io/gorm"]
DB --> PGX["jackc/pgx"]
Stor --> MinIO["minio/minio-go"]
Hdl --> Gin["gin-gonic/gin"]
Hdl --> Swagger["swaggo/gin-swagger"]
```
图表来源
- [go.mod](file://go.mod#L1-L92)
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
章节来源
- [go.mod](file://go.mod#L1-L92)
## 性能与运行建议
- 数据库连接池:通过环境变量覆盖最大空闲/活动连接数与连接生命周期,以适配不同负载
- Redis 池大小:根据并发与延迟需求调整
- 日志轮转:合理设置日志文件大小、保留份数与保留天数,避免磁盘压力
- Swagger 生成:仅在开发阶段生成,生产环境可预生成并禁用动态生成
[本节为通用建议,无需特定文件来源]
## 故障排除指南
### 环境变量检查
- 使用脚本检查 .env 是否存在、必需变量是否完整、敏感默认值是否已被修改
- 建议在本地复制 .env.example 为 .env 并按需填写
章节来源
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
### Swagger 文档生成失败
- 确认已安装 swag 工具go install github.com/swaggo/swag/cmd/swag@latest
- 使用 run.sh 或 run.bat 自动安装与生成
- 若使用 dev.sh确保 swag 可用且目标入口文件路径正确
章节来源
- [run.sh](file://run.sh#L1-L37)
- [run.bat](file://run.bat#L1-L42)
- [scripts/dev.sh](file://scripts/dev.sh#L1-L29)
### 数据库连接失败
- 检查 DATABASE_HOST、DATABASE_PORT、DATABASE_NAME、DATABASE_USERNAME、DATABASE_PASSWORD
- 确认数据库服务可达、SSL 模式与时区设置符合实际
- 查看日志模块输出定位错误
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
### 对象存储连接失败
- 检查 RUSTFS_ENDPOINT、RUSTFS_ACCESS_KEY、RUSTFS_SECRET_KEY、RUSTFS_USE_SSL
- 确认存储服务可用且网络连通
- 若仅部分功能受限,可在日志中看到警告
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
### JWT 密钥安全
- 确保 JWT_SECRET 长度足够且非默认值
- 生产环境务必更换默认密钥
章节来源
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
### 服务器启动与停止
- 正常启动:使用 run.sh 或 run.bat或直接 go run cmd/server/main.go
- 停止服务:在终端按 Ctrl+C 触发优雅关闭
章节来源
- [run.sh](file://run.sh#L1-L37)
- [run.bat](file://run.bat#L1-L42)
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
## 结论
通过本指南,你可以快速完成 CarrotSkin 后端的环境准备与项目启动。建议在本地先用 run.sh 或 run.bat 完成一键启动,随后使用 scripts/check-env.sh 校验环境变量,最后在开发过程中使用 dev.sh 进行迭代调试。遇到问题时,优先检查环境变量、数据库与对象存储连通性,并确认 swag 工具可用。
[本节为总结,无需特定文件来源]
## 附录
### 开发环境先决条件
- Go 版本:项目要求 Go 1.23;工具链为 1.24.2
- 数据库PostgreSQL驱动与连接参数见配置
- 对象存储S3 兼容服务RustFS Endpoint、AccessKey、SecretKey
- 其他Redis可选、邮件 SMTP可选
章节来源
- [go.mod](file://go.mod#L1-L92)
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
### 配置与环境变量清单
- 服务器SERVER_PORT、SERVER_MODE、SERVER_READ_TIMEOUT、SERVER_WRITE_TIMEOUT
- 数据库DATABASE_DRIVER、DATABASE_HOST、DATABASE_PORT、DATABASE_USERNAME、DATABASE_PASSWORD、DATABASE_NAME、DATABASE_SSL_MODE、DATABASE_TIMEZONE、DATABASE_MAX_IDLE_CONNS、DATABASE_MAX_OPEN_CONNS、DATABASE_CONN_MAX_LIFETIME
- RedisREDIS_HOST、REDIS_PORT、REDIS_PASSWORD、REDIS_DATABASE、REDIS_POOL_SIZE
- 对象存储RUSTFS_ENDPOINT、RUSTFS_ACCESS_KEY、RUSTFS_SECRET_KEY、RUSTFS_USE_SSL、RUSTFS_BUCKET_TEXTURES、RUSTFS_BUCKET_AVATARS
- JWTJWT_SECRET、JWT_EXPIRE_HOURS
- 日志LOG_LEVEL、LOG_FORMAT、LOG_OUTPUT、LOG_MAX_SIZE、LOG_MAX_BACKUPS、LOG_MAX_AGE、LOG_COMPRESS
- 上传UPLOAD_MAX_SIZE、UPLOAD_TEXTURE_MAX_SIZE、UPLOAD_AVATAR_MAX_SIZE、UPLOAD_ALLOWED_TYPES
- 邮件EMAIL_ENABLED、EMAIL_SMTP_HOST、EMAIL_SMTP_PORT、EMAIL_USERNAME、EMAIL_PASSWORD、EMAIL_FROM_NAME
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
### 启动脚本与命令说明
- dev.sh开发环境
- 自动复制配置示例为正式配置
- 执行 go mod tidy
- 检测并生成 Swagger 文档
- 启动应用
- run.sh一次性安装并启动
- 自动安装 swag
- 生成 Swagger 文档
- 启动应用并打印访问地址
- run.batWindows
- 自动安装 swag
- 生成 Swagger 文档
- 启动应用并打印访问地址
章节来源
- [scripts/dev.sh](file://scripts/dev.sh#L1-L29)
- [run.sh](file://run.sh#L1-L37)
- [run.bat](file://run.bat#L1-L42)
### Swagger 文档访问
- 服务地址http://localhost:8080
- Swagger 文档http://localhost:8080/swagger/index.html
- 健康检查http://localhost:8080/health
章节来源
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
- [run.sh](file://run.sh#L1-L37)
- [run.bat](file://run.bat#L1-L42)

View File

@@ -1,306 +0,0 @@
# 技术栈与依赖
<cite>
**本文档引用的文件**
- [go.mod](file://go.mod#L1-L92)
- [config.go](file://pkg/config/config.go#L1-L305)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
- [logger.go](file://pkg/logger/logger.go#L1-L69)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [cors.go](file://internal/middleware/cors.go#L1-L23)
- [swagger.go](file://internal/handler/swagger.go#L1-L63)
- [routes.go](file://internal/handler/routes.go#L1-L140)
- [rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
- [run.sh](file://run.sh#L1-L37)
- [start.sh](file://start.sh#L1-L41)
</cite>
## 目录
1. [技术选型概览](#技术选型概览)
2. [核心框架与库详解](#核心框架与库详解)
3. [技术栈集成架构](#技术栈集成架构)
4. [依赖关系与版本兼容性](#依赖关系与版本兼容性)
5. [配置管理机制](#配置管理机制)
6. [扩展点与优化建议](#扩展点与优化建议)
## 技术选型概览
CarrotSkin项目采用现代化的Go语言技术栈构建了一个高性能、可扩展的Minecraft皮肤站后端服务。项目技术选型遵循"简单、高效、可靠"的原则选择了业界广泛使用且维护良好的开源库。整体技术架构采用分层设计从前端API路由到后端数据存储各组件职责清晰耦合度低。
项目以Gin作为Web框架提供了高性能的HTTP路由和中间件支持使用GORM作为ORM框架简化了与PostgreSQL数据库的交互通过Redis实现高速缓存和会话管理采用MinIO/RustFS作为S3兼容的对象存储用于存储用户上传的皮肤和头像文件权限控制由Casbin提供支持灵活的基于角色的访问控制RBAC日志系统采用Zap提供结构化日志记录配置管理使用Viper支持从环境变量加载配置API文档通过Swag生成提供交互式文档界面。
这种技术组合不仅满足了项目当前的功能需求还为未来的扩展和维护提供了良好的基础。所有技术栈均通过Go Modules进行依赖管理确保了版本的确定性和可重现性。
**本节来源**
- [go.mod](file://go.mod#L1-L92)
- [run.sh](file://run.sh#L1-L37)
## 核心框架与库详解
### GinWeb框架
Gin是CarrotSkin项目的核心Web框架负责处理所有HTTP请求和响应。项目通过`internal/handler/routes.go`文件中的`RegisterRoutes`函数注册了完整的API路由体系包括用户认证、材质管理、档案服务等模块。Gin的中间件机制被充分利用实现了JWT认证、CORS跨域、请求日志记录和panic恢复等关键功能。
`internal/middleware/auth.go`中,实现了`AuthMiddleware`函数作为JWT认证中间件验证请求头中的Authorization令牌并将用户信息注入到请求上下文中。同时`internal/middleware/cors.go`提供了CORS中间件配置了允许的源、方法和头部确保前端应用能够正常访问API。
```mermaid
flowchart TD
Client["客户端请求"] --> CORS["CORS中间件"]
CORS --> Logger["日志中间件"]
Logger --> Recovery["恢复中间件"]
Recovery --> Auth["认证中间件"]
Auth --> Router["Gin路由器"]
Router --> Handler["业务处理函数"]
Handler --> Response["响应客户端"]
```
**本节来源**
- [routes.go](file://internal/handler/routes.go#L1-L140)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [cors.go](file://internal/middleware/cors.go#L1-L23)
### GORMORM
GORM作为对象关系映射框架负责与PostgreSQL数据库的交互。在`pkg/database/postgres.go`中,`New`函数创建了GORM数据库实例通过`gorm.Open`连接到PostgreSQL。项目配置了连接池参数包括最大空闲连接数、最大打开连接数和连接最大生命周期以优化数据库性能。
GORM的配置中禁用了外键约束自动创建`DisableForeignKeyConstraintWhenMigrating: true`以避免循环依赖问题。每个数据模型如用户、材质、档案等都定义了对应的GORM模型结构体并通过`TableName`方法指定数据库表名。GORM的链式API使得数据库查询操作简洁明了支持CRUD操作、关联查询和事务处理。
**本节来源**
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
- [audit_log.go](file://internal/model/audit_log.go#L24-L45)
### PostgreSQL数据库
PostgreSQL作为关系型数据库存储了用户信息、权限规则、系统配置等结构化数据。项目通过`pkg/config/config.go`中的`DatabaseConfig`结构体定义了数据库连接配置包括主机、端口、用户名、密码、数据库名、SSL模式和时区等。默认配置连接到本地的PostgreSQL实例时区设置为"Asia/Shanghai"。
数据库设计遵循规范化原则通过外键关联不同实体。例如用户表与材质表、档案表之间存在一对多关系。Casbin权限规则存储在`casbin_rule`表中通过GORM适配器与Casbin库集成。审计日志记录在`audit_logs`表中,用于追踪关键操作。
**本节来源**
- [config.go](file://pkg/config/config.go#L34-L47)
### Redis缓存
Redis在项目中扮演着多重角色缓存验证码、存储会话数据、计数器等。`pkg/redis/redis.go`中的`Client`结构体包装了`github.com/redis/go-redis/v9`客户端,提供了类型安全的方法封装。`New`函数创建Redis连接并配置了连接超时、读写超时和连接池大小。
Redis客户端支持多种数据结构操作包括字符串Set/Get、哈希HSet/HGet、集合SAdd/SMembers和有序集合ZAdd/ZRange。在用户服务中Redis用于存储验证码通过`Set`方法设置带过期时间的键值对在Yggdrasil服务中用于存储服务器加入会话确保Minecraft服务器验证的安全性。
```mermaid
classDiagram
class RedisClient {
+Client *redis.Client
+logger *zap.Logger
+Set(ctx, key, value, expiration) error
+Get(ctx, key) (string, error)
+HSet(ctx, key, values) error
+HGet(ctx, key, field) (string, error)
+SAdd(ctx, key, members) error
+SMembers(ctx, key) ([]string, error)
+Close() error
}
class RedisConfig {
+Host string
+Port int
+Password string
+Database int
+PoolSize int
}
RedisClient --> RedisConfig : "使用"
```
**本节来源**
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [config.go](file://pkg/config/config.go#L49-L56)
### MinIO/RustFS对象存储
MinIO/RustFS作为S3兼容的对象存储用于存储用户上传的皮肤文件和头像。`pkg/storage/minio.go`中的`StorageClient`结构体封装了`github.com/minio/minio-go/v7`客户端支持所有S3兼容的存储服务。`NewStorage`函数创建存储客户端,通过`minio.New`连接到指定的Endpoint。
项目采用预签名URLPresigned URL机制实现安全的文件上传。`GeneratePresignedURL`方法生成一个带有时限的PUT URL前端可以直接使用该URL上传文件到指定的存储桶而无需经过后端服务器中转大大减轻了服务器带宽压力。`GeneratePresignedPostURL`方法支持表单上传,提供了更灵活的上传方式。
```mermaid
sequenceDiagram
participant Frontend as 前端
participant Backend as 后端
participant Storage as 对象存储
Frontend->>Backend : 请求上传URL
Backend->>Storage : 生成预签名URL
Storage-->>Backend : 返回预签名URL
Backend-->>Frontend : 返回预签名URL
Frontend->>Storage : 直接上传文件
Storage-->>Frontend : 上传成功
```
**本节来源**
- [minio.go](file://pkg/storage/minio.go#L1-L121)
- [config.go](file://pkg/config/config.go#L58-L65)
### Casbin权限控制
Casbin提供基于模型的访问控制ABAC支持多种权限模型如ACL、RBAC、ABAC等。CarrotSkin项目采用RBAC基于角色的访问控制模型定义在`configs/casbin/rbac_model.conf`文件中。该模型包含请求定义、策略定义、角色定义、匹配器和策略效果。
在RBAC模型中权限由主体用户、客体资源和动作操作三元组组成。通过`g(r.sub, p.sub)`表达式,实现了角色继承,即用户可以继承其角色的权限。权限策略存储在数据库的`casbin_rule`表中通过GORM适配器与Casbin集成实现了动态权限管理。
**本节来源**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
- [audit_log.go](file://internal/model/audit_log.go#L29-L45)
### Zap日志
Zap是Uber开源的高性能日志库提供结构化日志记录。`pkg/logger/logger.go`中的`New`函数根据配置创建Zap日志实例支持JSON和控制台两种输出格式。日志级别可配置为debug、info、warn或error默认为info。
日志记录包含时间戳、日志级别、消息以及结构化字段如HTTP请求的方法、路径、状态码、延迟、客户端IP和用户代理。日志输出可以重定向到文件支持按大小轮转最大保留28天的日志文件。在中间件中Zap被用于记录每个HTTP请求的详细信息便于问题排查和性能分析。
**本节来源**
- [logger.go](file://pkg/logger/logger.go#L1-L69)
- [config.go](file://pkg/config/config.go#L79-L88)
### Viper配置管理
Viper是Go语言的配置管理库支持多种配置源和格式。CarrotSkin项目完全从环境变量加载配置不依赖配置文件。`pkg/config/config.go`中的`Load`函数使用Viper从环境变量中解析配置环境变量前缀为"CARROTSKIN"。
项目定义了`Config`结构体包含服务器、数据库、Redis、对象存储、JWT、Casbin、日志、上传和邮件等模块的配置。通过`viper.SetDefault`设置默认值,确保在环境变量缺失时仍能正常运行。`setupEnvMappings`函数绑定环境变量到配置字段,实现了灵活的配置映射。
**本节来源**
- [config.go](file://pkg/config/config.go#L13-L305)
### SwagAPI文档
Swag通过解析Go代码中的注释自动生成Swagger/OpenAPI规范。`internal/handler/swagger.go`文件包含API文档的元信息如标题、版本、描述、联系人、许可证和主机地址。每个API端点通过注释定义包括路径、方法、参数、请求体、响应和安全定义。
`run.sh`脚本中的`swag init`命令生成Swagger文档输出到`docs`目录。`SetupSwagger`函数注册Swagger UI路由使开发者可以通过浏览器访问交互式API文档。API文档不仅提高了开发效率还为前端团队提供了清晰的接口契约。
**本节来源**
- [swagger.go](file://internal/handler/swagger.go#L1-L63)
- [run.sh](file://run.sh#L22-L27)
## 技术栈集成架构
CarrotSkin项目的技术栈集成体现了清晰的分层架构和关注点分离原则。整个系统可以分为四层API层、服务层、数据访问层和外部服务层。
```mermaid
graph TD
A["API层"] --> B["服务层"]
B --> C["数据访问层"]
C --> D["外部服务层"]
subgraph API层
A1[Gin Web框架]
A2[路由注册]
A3[中间件]
A4[Swagger文档]
end
subgraph 服务层
B1[用户服务]
B2[材质服务]
B3[档案服务]
B4[验证码服务]
end
subgraph 数据访问层
C1[GORM ORM]
C2[PostgreSQL]
C3[Redis客户端]
C4[对象存储客户端]
end
subgraph 外部服务层
D1[PostgreSQL数据库]
D2[Redis服务器]
D3[MinIO/RustFS]
end
A1 --> B1
A1 --> B2
A1 --> B3
A1 --> B4
B1 --> C1
B2 --> C1
B3 --> C1
B4 --> C3
C1 --> D1
C3 --> D2
C4 --> D3
```
API层由Gin框架驱动处理HTTP请求执行中间件链CORS、日志、认证、恢复然后将请求分发到相应的服务层。服务层包含业务逻辑协调多个数据访问组件完成特定功能。数据访问层封装了与外部服务的交互细节提供统一的接口给服务层使用。外部服务层是独立部署的基础设施通过网络与应用层通信。
这种架构的优势在于各层之间松耦合便于独立开发和测试服务层专注于业务逻辑不关心数据存储细节数据访问层可以轻松替换底层实现如从PostgreSQL迁移到MySQL外部服务的故障隔离不会直接影响应用核心逻辑。
**本节来源**
- [routes.go](file://internal/handler/routes.go#L1-L140)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
## 依赖关系与版本兼容性
项目的依赖关系通过Go Modules进行管理`go.mod`文件明确指定了所有直接依赖的版本。核心依赖的版本选择考虑了稳定性、性能和功能需求:
- **Gin v1.9.1**稳定版本提供了完整的Web框架功能
- **GORM v1.25.5**支持PostgreSQL的最新特性如JSONB类型
- **PostgreSQL驱动 v1.5.4**GORM官方推荐的PostgreSQL驱动
- **Redis客户端 v9.0.5**支持Redis 6+的新特性如ACL
- **MinIO Go SDK v7.0.66**支持S3兼容存储的最新API
- **Casbin v1.16.2**:提供了丰富的权限模型和适配器
- **Zap v1.26.0**:高性能日志库,支持结构化日志
- **Viper v1.21.0**:功能完整的配置管理库
- **Swag v1.16.2**支持最新Swagger规范
所有依赖都通过`require`指令声明间接依赖由Go Modules自动解析。版本号采用语义化版本控制确保了向后兼容性。项目使用Go 1.23.0版本,与所有依赖库兼容。
依赖的初始化遵循单例模式,通过`sync.Once`确保只初始化一次。例如,`pkg/auth/manager.go`中的`Init`函数使用`sync.Once`保证JWT服务只创建一次避免了并发初始化问题。这种设计模式提高了系统的稳定性和资源利用率。
**本节来源**
- [go.mod](file://go.mod#L1-L92)
- [manager.go](file://pkg/auth/manager.go#L1-L46)
## 配置管理机制
CarrotSkin项目采用环境变量作为唯一的配置源不使用配置文件这符合12-Factor应用的方法论。`pkg/config/config.go`中的`Load`函数是配置管理的核心,它首先加载`.env`文件(如果存在),然后从环境变量中解析配置。
配置结构体`Config`包含所有模块的配置,通过`mapstructure`标签映射到Viper的键。`setDefaults`函数设置合理的默认值,确保应用在最小配置下也能运行。`setupEnvMappings`函数显式绑定环境变量到配置字段,提高了配置的可读性和可维护性。
敏感配置如数据库密码、JWT密钥必须通过环境变量提供避免硬编码在代码中。`start.sh`脚本展示了生产环境的配置示例包括数据库、Redis、对象存储和JWT的详细配置。这种配置方式便于在不同环境开发、测试、生产之间切换只需更改环境变量即可。
```mermaid
flowchart TD
A["配置源"] --> B[".env文件"]
A --> C["环境变量"]
B --> D["加载.env文件"]
C --> E["读取环境变量"]
D --> F["设置默认值"]
E --> F
F --> G["解析到Config结构体"]
G --> H["覆盖特定配置"]
H --> I["返回配置实例"]
```
**本节来源**
- [config.go](file://pkg/config/config.go#L108-L305)
- [start.sh](file://start.sh#L1-L41)
## 扩展点与优化建议
CarrotSkin项目的技术栈为未来的扩展提供了良好的基础。以下是几个潜在的扩展点和优化建议
1. **数据库读写分离**随着用户量增长可以引入数据库读写分离将读请求分发到只读副本减轻主库压力。GORM支持多数据库连接可以轻松实现。
2. **缓存策略优化**当前Redis主要用于验证码和会话可以扩展为数据缓存层缓存热点数据如用户资料、材质列表减少数据库查询。
3. **消息队列集成**对于耗时操作如邮件发送、文件处理可以引入消息队列如RabbitMQ、Kafka实现异步处理提高响应速度。
4. **监控与告警**集成Prometheus和Grafana收集应用性能指标如请求延迟、错误率、数据库连接数设置告警规则及时发现和解决问题。
5. **容器化部署**将应用打包为Docker镜像使用Kubernetes进行编排实现高可用和弹性伸缩。
6. **多存储后端支持**当前对象存储基于S3兼容协议可以扩展支持其他云存储如AWS S3、阿里云OSS提供存储后端选择。
7. **权限模型增强**在RBAC基础上引入ABAC基于属性的访问控制实现更细粒度的权限控制。
8. **API版本管理**随着API演进引入版本管理`/api/v2`),确保向后兼容,平滑过渡。
这些扩展点可以在不影响现有功能的前提下逐步实施,持续提升系统的性能、可靠性和可维护性。
**本节来源**
- [config.go](file://pkg/config/config.go#L1-L305)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [minio.go](file://pkg/storage/minio.go#L1-L121)

View File

@@ -1,546 +0,0 @@
# 数据模型
<cite>
**本文引用的文件**
- [internal/model/user.go](file://internal/model/user.go)
- [internal/model/texture.go](file://internal/model/texture.go)
- [internal/model/profile.go](file://internal/model/profile.go)
- [internal/model/system_config.go](file://internal/model/system_config.go)
- [internal/model/audit_log.go](file://internal/model/audit_log.go)
- [internal/model/token.go](file://internal/model/token.go)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
- [pkg/database/manager.go](file://pkg/database/manager.go)
- [pkg/database/postgres.go](file://pkg/database/postgres.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向 CarrotSkin 项目的数据库层,聚焦于核心实体 User、Texture、Profile 和 SystemConfig 的数据模型设计。内容涵盖字段定义、数据类型、主键/外键关系、索引与约束、业务语义与验证规则并提供实体关系图ERD、示例数据与数据访问模式说明帮助初学者快速理解同时为有经验的开发者提供性能优化与迁移策略建议。
## 项目结构
围绕数据模型的关键目录与文件:
- 模型层internal/model 下的各实体模型文件
- 仓储层internal/repository 下的 CRUD 与聚合查询实现
- 数据库层pkg/database 提供连接、迁移与连接池管理
```mermaid
graph TB
subgraph "模型层"
MUser["User<br/>internal/model/user.go"]
MTexture["Texture<br/>internal/model/texture.go"]
MProfile["Profile<br/>internal/model/profile.go"]
MSystemConfig["SystemConfig<br/>internal/model/system_config.go"]
MAudit["AuditLog<br/>internal/model/audit_log.go"]
MToken["Token<br/>internal/model/token.go"]
MYgg["Yggdrasil<br/>internal/model/yggdrasil.go"]
end
subgraph "仓储层"
RUser["UserRepository<br/>internal/repository/user_repository.go"]
RTexture["TextureRepository<br/>internal/repository/texture_repository.go"]
RProfile["ProfileRepository<br/>internal/repository/profile_repository.go"]
RSystem["SystemConfigRepository<br/>internal/repository/system_config_repository.go"]
end
subgraph "数据库层"
DManager["Database Manager<br/>pkg/database/manager.go"]
DPostgres["Postgres Driver<br/>pkg/database/postgres.go"]
end
MUser --> RUser
MTexture --> RTexture
MProfile --> RProfile
MSystemConfig --> RSystem
RUser --> DManager
RTexture --> DManager
RProfile --> DManager
RSystem --> DManager
DManager --> DPostgres
```
图表来源
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
- [internal/model/audit_log.go](file://internal/model/audit_log.go#L1-L46)
- [internal/model/token.go](file://internal/model/token.go#L1-L15)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L48)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
章节来源
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
## 核心组件
本节对四个核心实体进行字段级说明,包括数据类型、约束、索引与业务含义。
- User用户
- 主键id自增整数
- 唯一索引username、email
- 字段要点:
- username字符串唯一用于登录与标识
- email字符串唯一用于找回密码与通知
- avatar字符串头像 URL
- points整数积分余额支持增减与日志追踪
- role字符串默认“user”用于权限控制
- status小整数1 正常、0 禁用、-1 删除(软删除)
- propertiesJSONB存储扩展属性
- last_login_at时间戳最近登录时间
- created_at/updated_at时间戳默认 CURRENT_TIMESTAMP
- 业务规则:
- 软删除通过 status 字段实现
- 登录日志与积分日志分别记录在 user_login_logs 与 user_point_logs
- Texture材质
- 主键id自增整数
- 外键uploader_id → User.id
- 唯一索引hashSHA-256
- 字段要点:
- uploader_id整数上传者
- name/description名称与描述
- type枚举SKIN 或 CAPE
- url字符串材质资源地址
- hash字符串64唯一用于去重
- size整数字节数
- is_public布尔是否公开
- download_count/favorite_count整数统计指标带索引
- is_slim布尔是否 Alex模型
- status小整数1 正常、0 审核中、-1 删除(软删除)
- created_at/updated_at时间戳
- 业务规则:
- is_public + type + status 组合索引用于检索
- 下载与收藏计数采用表达式更新,避免并发竞争导致的丢失更新
- Profile档案
- 主键uuid字符串36 位)
- 外键user_id → User.id
- 唯一索引name角色名
- 字段要点:
- uuid档案 UUID
- user_id整数所属用户
- name字符串Minecraft 角色名,唯一
- skin_id/cape_id整数关联 Texture.id
- rsa_private_key文本RSA 私钥(不对外返回)
- is_active布尔是否为当前激活档案
- last_used_at时间戳最近使用时间
- created_at/updated_at时间戳
- 业务规则:
- 激活档案切换时,同一用户下其他档案会被置为非激活
- 支持预加载 Skin/Cape 关联实体
- SystemConfig系统配置
- 主键id自增整数
- 唯一索引key
- 字段要点:
- key字符串配置键唯一
- value文本配置值
- description字符串描述
- type枚举STRING/INTEGER/BOOLEAN/JSON
- is_public布尔是否允许前端读取
- created_at/updated_at时间戳
- 业务规则:
- is_public 为 true 的配置可作为公开响应的一部分返回
章节来源
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
- [internal/model/user.go](file://internal/model/user.go#L28-L71)
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
- [internal/model/texture.go](file://internal/model/texture.go#L42-L77)
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L32)
## 架构总览
数据库层通过连接管理器统一初始化与迁移,模型定义通过 GORM 注解映射到 PostgreSQL 表结构。迁移顺序遵循“先被引用表,后引用表”的原则,确保外键约束可用。
```mermaid
sequenceDiagram
participant App as "应用启动"
participant DBMgr as "Database Manager"
participant PG as "Postgres Driver"
participant ORM as "GORM"
participant Repo as "Repositories"
App->>DBMgr : Init(cfg, logger)
DBMgr->>PG : New(cfg)
PG-->>DBMgr : *gorm.DB
DBMgr->>ORM : AutoMigrate(models...)
ORM-->>DBMgr : 迁移完成
App->>Repo : 使用仓储层进行CRUD
Repo->>ORM : 执行查询/更新
ORM-->>Repo : 结果
```
图表来源
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L99)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
## 详细组件分析
### 实体关系图ERD
以下 ERD 展示了 User、Texture、Profile、SystemConfig 的主键、外键与关键索引。
```mermaid
erDiagram
USER {
bigint id PK
varchar username UK
varchar email UK
varchar avatar
integer points
varchar role
smallint status
jsonb properties
timestamp last_login_at
timestamp created_at
timestamp updated_at
}
TEXTURE {
bigint id PK
bigint uploader_id FK
varchar name
text description
varchar type
varchar url
varchar hash UK
integer size
boolean is_public
integer download_count
integer favorite_count
boolean is_slim
smallint status
timestamp created_at
timestamp updated_at
}
PROFILE {
varchar uuid PK
bigint user_id FK
varchar name UK
bigint skin_id
bigint cape_id
text rsa_private_key
boolean is_active
timestamp last_used_at
timestamp created_at
timestamp updated_at
}
SYSTEM_CONFIG {
bigint id PK
varchar key UK
text value
varchar description
varchar type
boolean is_public
timestamp created_at
timestamp updated_at
}
USER ||--o{ TEXTURE : "uploader_id -> id"
USER ||--o{ PROFILE : "user_id -> id"
TEXTURE ||--o{ PROFILE : "skin_id/cape_id -> id"
```
图表来源
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L32)
### User 模型与访问模式
- 字段与约束
- 主键id
- 唯一索引username、email
- 状态软删除status=-1 表示删除
- JSONB 扩展属性properties
- 关联与日志
- UserPointLog记录积分变动含 operator_id 关联操作人
- UserLoginLog记录登录来源与结果
- 仓储能力
- 基础 CRUD、按用户名/邮箱查询、软删除
- 事务内更新积分并写入日志
- 更新头像、邮箱等字段
```mermaid
sequenceDiagram
participant Svc as "服务层"
participant Repo as "UserRepository"
participant DB as "GORM"
participant Log as "UserPointLog"
Svc->>Repo : UpdateUserPoints(userID, amount, type, reason)
Repo->>DB : 事务开始
Repo->>DB : 查询用户当前积分
Repo->>DB : 校验余额防止负值
Repo->>DB : 更新用户积分
Repo->>DB : 创建积分日志(Log)
DB-->>Repo : 提交事务
Repo-->>Svc : 返回结果
```
图表来源
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
- [internal/model/user.go](file://internal/model/user.go#L28-L71)
章节来源
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/model/user.go](file://internal/model/user.go#L7-L71)
### Texture 模型与访问模式
- 字段与约束
- 主键id唯一索引hash
- 外键uploader_id → User.id
- 组合索引is_public + type + statusdownload_count/favorite_count 带索引
- 关联与日志
- UserTextureFavorite用户收藏材质联合唯一索引 uk_user_texture
- TextureDownloadLog下载记录
- 仓储能力
- 创建、按 ID/Hash 查询、分页与搜索(关键词、类型、公开度)
- 软删除status=-1
- 表达式更新下载/收藏计数,避免竞态
- 收藏/取消收藏与查询用户收藏列表
```mermaid
flowchart TD
Start(["搜索入口"]) --> BuildQuery["构建基础查询<br/>status=1"]
BuildQuery --> Public{"仅公开?"}
Public --> |是| ApplyPublic["追加 is_public=true"]
Public --> |否| SkipPublic["跳过公开过滤"]
ApplyPublic --> TypeFilter{"指定类型?"}
SkipPublic --> TypeFilter
TypeFilter --> |是| ApplyType["追加 type=?"]
TypeFilter --> |否| KeywordFilter
ApplyType --> KeywordFilter{"有关键词?"}
KeywordFilter --> |是| ApplyKeyword["name/description LIKE %keyword%"]
KeywordFilter --> |否| Paginate
ApplyKeyword --> Paginate["COUNT + ORDER BY created_at DESC + 分页"]
Paginate --> End(["返回结果"])
```
图表来源
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
章节来源
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
### Profile 模型与访问模式
- 字段与约束
- 主键uuid唯一索引name
- 外键user_id → User.idskin_id/cape_id → Texture.id
- is_active 控制当前生效档案
- 仓储能力
- 创建、按 uuid/name 查询、按用户查询全部档案
- 设置激活档案(事务内将用户其他档案置为非激活)
- 更新最后使用时间
- KeyPair 的读写JSONB 字段)
```mermaid
sequenceDiagram
participant Svc as "服务层"
participant Repo as "ProfileRepository"
participant DB as "GORM"
Svc->>Repo : SetActiveProfile(uuid, userID)
Repo->>DB : 事务开始
Repo->>DB : 将用户所有档案 is_active=false
Repo->>DB : 将指定档案 is_active=true
DB-->>Repo : 提交事务
Repo-->>Svc : 返回结果
```
图表来源
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
章节来源
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
### SystemConfig 模型与访问模式
- 字段与约束
- 主键id唯一索引key
- 类型枚举STRING/INTEGER/BOOLEAN/JSON
- is_public 控制前端可见性
- 仓储能力
- 按 key 查询、获取公开配置、获取全部配置、更新值
章节来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
### 其他相关模型与迁移顺序
- AuditLog审计日志记录用户行为与资源变更含 JSONB 字段与多维索引
- Token认证令牌模型表名为 token用于 Yggdrasil 等流程
- Yggdrasil与 User 一对一关联User 创建后自动同步生成随机密码记录
迁移顺序AutoMigrate
- 先创建被引用表User、UserPointLog、UserLoginLog
- 再创建引用表Profile、Texture、UserTextureFavorite、TextureDownloadLog、Token、Yggdrasil
- 最后创建 SystemConfig、AuditLog、CasbinRule
章节来源
- [internal/model/audit_log.go](file://internal/model/audit_log.go#L1-L46)
- [internal/model/token.go](file://internal/model/token.go#L1-L15)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L48)
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
## 依赖分析
- 模型到仓储:各模型通过 GORM 注解与仓储层交互,仓储层负责具体查询、更新与事务控制
- 仓储到数据库:统一通过 MustGetDB 获取连接,避免重复初始化
- 迁移顺序AutoMigrate 明确列出迁移顺序,确保外键约束可用
```mermaid
graph LR
MUser["User Model"] --> RUser["UserRepository"]
MTexture["Texture Model"] --> RTexture["TextureRepository"]
MProfile["Profile Model"] --> RProfile["ProfileRepository"]
MSystem["SystemConfig Model"] --> RSystem["SystemConfigRepository"]
RUser --> DMgr["Database Manager"]
RTexture --> DMgr
RProfile --> DMgr
RSystem --> DMgr
DMgr --> DPostgres["Postgres Driver"]
```
图表来源
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [pkg/database/manager.go](file://pkg/database/manager.go#L35-L99)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
## 性能考量
- 索引策略
- Userusername、email 唯一索引;登录/积分日志按 created_at 倒序索引
- Texturehash 唯一索引is_public + type + status 组合索引download_count/favorite_count 带索引
- Profilename 唯一索引user_id 索引
- SystemConfigkey 唯一索引is_public 索引
- AuditLog多维索引action、resource_type+resource_id、created_at
- 并发与一致性
- 使用表达式更新计数字段download_count、favorite_count避免竞态
- 事务内更新用户积分并写入日志,保证原子性
- 连接池与日志
- 连接池参数由配置注入,建议结合负载压测调整 MaxOpenConns、MaxIdleConns、ConnMaxLifetime
- 日志级别在 Postgres 驱动中按驱动类型配置
章节来源
- [internal/model/user.go](file://internal/model/user.go#L28-L71)
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
- [internal/model/texture.go](file://internal/model/texture.go#L42-L77)
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L32)
- [internal/model/audit_log.go](file://internal/model/audit_log.go#L7-L27)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
## 故障排查指南
- 数据库未初始化
- 现象:调用 MustGetDB 抛错或 AutoMigrate 返回错误
- 排查:确认已调用 Init(cfg, logger),检查配置项与连接可达性
- 迁移失败
- 现象AutoMigrate 报错
- 排查:检查迁移顺序是否正确(先被引用表,后引用表);确认数据库版本与驱动兼容
- 查询不到记录
- 现象:按 username/email/uuid 查询返回空
- 排查:确认 status 非 -1检查唯一索引是否冲突确认大小写与格式
- 并发计数不一致
- 现象download_count/favorite_count 不准确
- 排查:确认使用表达式更新;避免直接赋值覆盖
- 事务回滚
- 现象:积分更新失败或日志未写入
- 排查:检查事务内错误处理与提交路径
章节来源
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L33)
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
## 结论
本数据模型围绕 User、Texture、Profile、SystemConfig 四大核心实体,采用 PostgreSQL + GORM 的组合实现,具备完善的索引与约束策略、清晰的迁移顺序与事务保障。通过仓储层抽象,实现了稳定的 CRUD 与复杂查询能力。建议在生产环境中结合监控与压测持续优化连接池与索引策略。
## 附录
### 示例数据(概念性)
- User
- id: 1
- username: "alice"
- email: "alice@example.com"
- points: 100
- role: "user"
- status: 1
- properties: "{}"
- last_login_at: 当前时间
- created_at/updated_at: 当前时间
- Texture
- id: 101
- uploader_id: 1
- name: "Steve Classic"
- type: "SKIN"
- url: "/uploads/skin_101.png"
- hash: "sha256..."
- size: 102400
- is_public: true
- download_count: 120
- favorite_count: 45
- is_slim: false
- status: 1
- created_at/updated_at: 当前时间
- Profile
- uuid: "f47ac10b-62d6-4c6f-8b3c-1234567890ab"
- user_id: 1
- name: "Steve"
- skin_id: 101
- cape_id: null
- rsa_private_key: "..."
- is_active: true
- last_used_at: 当前时间
- created_at/updated_at: 当前时间
- SystemConfig
- id: 1
- key: "site_name"
- value: "CarrotSkin"
- description: "站点名称"
- type: "STRING"
- is_public: true
- created_at/updated_at: 当前时间
### 数据访问模式清单
- User
- 创建/查询/更新/软删除
- 事务内更新积分并写入日志
- Texture
- 创建/查询/按 Hash 去重/分页搜索/软删除
- 表达式更新下载/收藏计数
- 收藏/取消收藏与查询收藏列表
- Profile
- 创建/查询/按用户查询/设置激活档案/更新最后使用时间
- KeyPair 读写JSONB
- SystemConfig
- 按 key 查询/获取公开配置/获取全部配置/更新值
章节来源
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)

View File

@@ -1,408 +0,0 @@
# 材质模型
<cite>
**本文引用的文件**
- [internal/model/texture.go](file://internal/model/texture.go)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql)
- [internal/types/common.go](file://internal/types/common.go)
- [internal/model/user.go](file://internal/model/user.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件围绕材质模型进行系统化技术文档整理,覆盖以下主题:
- Texture 结构体及关联类型TextureType、UserTextureFavorite、TextureDownloadLog的字段语义与实现细节
- 材质类型SKIN/CAPE、哈希值Hash、URL 存储、尺寸与 Slim 模型标识的技术实现
- 材质状态机正常、审核中、已删除与公开性控制IsPublic的业务逻辑
- 与用户Uploader、收藏系统、下载日志的关联关系与索引策略
- 材质元数据管理、下载计数器并发更新优化、收藏功能去重机制的实践指导
- 基于 GORM 的复杂查询模式示例与最佳实践
## 项目结构
材质模型相关代码分布于模型层、仓储层与服务层,并通过数据库初始化脚本定义表结构与索引。
```mermaid
graph TB
subgraph "模型层"
M1["internal/model/texture.go<br/>定义 Texture/TextureType/UserTextureFavorite/TextureDownloadLog"]
M2["internal/model/user.go<br/>定义 User 及其关联"]
end
subgraph "仓储层"
R1["internal/repository/texture_repository.go<br/>材质 CRUD、搜索、计数器更新、收藏与下载日志"]
end
subgraph "服务层"
S1["internal/service/texture_service.go<br/>材质创建/更新/删除/搜索、下载记录、收藏切换、上传限制检查"]
end
subgraph "数据库"
D1["scripts/carrotskin_postgres.sql<br/>表结构、索引、约束、触发器"]
end
M1 --> R1
M2 --> R1
S1 --> R1
R1 --> D1
```
图表来源
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L1-L344)
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L1-L344)
## 核心组件
- Texture材质主实体包含上传者、名称、描述、类型、URL、哈希、尺寸、公开性、下载/收藏计数、Slim 标识、状态与时间戳,并与 User 建立外键关联
- TextureType材质类型枚举SKIN/CAPE
- UserTextureFavorite用户对材质的收藏关系具备联合唯一索引以保证去重
- TextureDownloadLog材质下载记录包含用户、IP、UA、时间等信息并与 Texture/User 建立外键关联
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/types/common.go](file://internal/types/common.go#L127-L152)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
## 架构总览
材质模型在服务层完成业务编排,在仓储层封装数据库访问,在模型层定义数据结构与关系。数据库层面通过外键、索引与触发器保障一致性与性能。
```mermaid
classDiagram
class Texture {
+int64 ID
+int64 UploaderID
+string Name
+string Description
+TextureType Type
+string URL
+string Hash
+int Size
+bool IsPublic
+int DownloadCount
+int FavoriteCount
+bool IsSlim
+int16 Status
+time CreatedAt
+time UpdatedAt
}
class User {
+int64 ID
+string Username
+string Email
+string Avatar
+int Points
+string Role
+int16 Status
+string Properties
+time LastLoginAt
+time CreatedAt
+time UpdatedAt
}
class UserTextureFavorite {
+int64 ID
+int64 UserID
+int64 TextureID
+time CreatedAt
}
class TextureDownloadLog {
+int64 ID
+int64 TextureID
+*int64 UserID
+string IPAddress
+string UserAgent
+time CreatedAt
}
Texture --> User : "Uploader"
UserTextureFavorite --> User : "User"
UserTextureFavorite --> Texture : "Texture"
TextureDownloadLog --> Texture : "Texture"
TextureDownloadLog --> User : "User"
```
图表来源
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
## 详细组件分析
### Texture 字段与业务语义
- 类型与哈希
- Type材质类型取值为 SKIN 或 CAPE
- Hash材质文件的 SHA-256 哈希,作为全局唯一标识,用于去重与校验
- 存储与元数据
- URL材质在对象存储中的访问地址
- Size文件大小字节
- IsSlimSlim 模型标识Alex 细臂为 trueSteve 粗臂为 false
- 公开性与状态
- IsPublic是否公开到皮肤广场
- Status状态机1 正常、0 审核中、-1 已删除)
- 计数器与排序
- DownloadCount、FavoriteCount下载与收藏计数分别建立降序索引以便高效排序与统计
- 时间戳与索引
- CreatedAt/UpdatedAt自动维护
- UploaderID、IsPublic+Type+Status、DownloadCount/FavoriteCount 等索引支撑查询与排序
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L85)
### TextureType 与 Slim 模型
- TextureType 枚举在模型与类型定义中保持一致,确保序列化与校验的一致性
- Slim 模型标识用于区分 Alex 与 Steve 模型,便于客户端渲染与兼容
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L7-L13)
- [internal/types/common.go](file://internal/types/common.go#L127-L152)
### 状态机与公开性控制
- 状态机
- 1正常可用
- 0审核中可视为待审状态
- -1已删除软删除
- 公开性控制
- IsPublic 控制是否对外可见
- 查询侧影响
- 搜索接口默认仅返回状态为正常的材质
- 用户上传列表过滤掉已删除材质
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L28-L31)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L93-L103)
### 与用户的关联关系
- 上传者关联
- Texture 与 User 通过 UploaderID 外键关联,查询时可预加载上传者信息
- 角色档案关联
- Profiles 表通过 skin_id/cape_id 引用 textures形成角色与材质的绑定关系
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L33-L35)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L111-L127)
### 收藏系统UserTextureFavorite
- 关系模型
- 用户与材质的多对多中间表,字段包含 user_id、texture_id、created_at
- 联合唯一索引 uk_user_texture保证同一用户对同一材质只能收藏一次
- 业务流程
- 切换收藏:先检查是否已收藏,再执行新增或删除,并同步更新材质的收藏计数
- 收藏列表子查询获取用户收藏的材质ID再按创建时间倒序分页查询
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Service as "TextureService"
participant Repo as "TextureRepository"
participant DB as "数据库"
Client->>Service : "ToggleTextureFavorite(userID, textureID)"
Service->>Repo : "FindTextureByID(textureID)"
Repo->>DB : "SELECT * FROM textures WHERE id=?"
DB-->>Repo : "Texture"
Repo-->>Service : "Texture"
Service->>Repo : "IsTextureFavorited(userID, textureID)"
Repo->>DB : "SELECT COUNT(*) FROM user_texture_favorites WHERE user_id=? AND texture_id=?"
DB-->>Repo : "count"
Repo-->>Service : "bool"
alt 已收藏
Service->>Repo : "RemoveTextureFavorite(userID, textureID)"
Repo->>DB : "DELETE FROM user_texture_favorites WHERE user_id=? AND texture_id=?"
Service->>Repo : "DecrementTextureFavoriteCount(textureID)"
Repo->>DB : "UPDATE textures SET favorite_count=favorite_count-1 WHERE id=?"
else 未收藏
Service->>Repo : "AddTextureFavorite(userID, textureID)"
Repo->>DB : "INSERT INTO user_texture_favorites(user_id, texture_id)"
Service->>Repo : "IncrementTextureFavoriteCount(textureID)"
Repo->>DB : "UPDATE textures SET favorite_count=favorite_count+1 WHERE id=?"
end
Service-->>Client : "返回收藏状态"
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L201)
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L42-L57)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L201)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
### 下载日志TextureDownloadLog
- 记录每次下载的材质、用户、IP、UA、时间等信息
- 下载计数器采用 GORM 表达式更新,避免并发写冲突导致的计数不准
- 提供按时间倒序索引,便于统计与审计
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Service as "TextureService"
participant Repo as "TextureRepository"
participant DB as "数据库"
Client->>Service : "RecordTextureDownload(textureID, userID, ip, ua)"
Service->>Repo : "FindTextureByID(textureID)"
Repo->>DB : "SELECT * FROM textures WHERE id=?"
DB-->>Repo : "Texture"
Service->>Repo : "IncrementTextureDownloadCount(textureID)"
Repo->>DB : "UPDATE textures SET download_count=download_count+1 WHERE id=?"
Service->>Repo : "CreateTextureDownloadLog(log)"
Repo->>DB : "INSERT INTO texture_download_logs(texture_id, user_id, ip_address, user_agent)"
Service-->>Client : "成功"
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L157)
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L59-L76)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L157)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L271-L291)
### GORM 使用示例与复杂查询模式
- 创建材质
- 参数校验与去重:先检查用户存在与哈希唯一性,再创建材质
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
- 搜索材质
- 过滤条件:状态=正常、公开筛选、类型筛选、关键词模糊匹配
- 分页与总数:先 Count 再分页查询,支持预加载上传者
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- 更新材质
- 权限校验:仅上传者可更新
- 动态字段更新:根据传入字段选择性更新
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L141)
- 删除材质(软删除)
- 仅将状态置为 -1保留历史数据与日志
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L131)
- 收藏切换
- 原子性:先判断是否已收藏,再执行新增/删除并更新计数
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
- 收藏列表
- 子查询先查出用户收藏的材质ID集合再查询材质并分页
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221)
- 上传限制检查
- 统计用户已上传材质数量,与系统配置的最大值比较
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L252)
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L141)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L131)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L252)
## 依赖分析
- 模型层依赖
- Texture 依赖 TextureType、User
- UserTextureFavorite 依赖 User、Texture
- TextureDownloadLog 依赖 User、Texture
- 仓储层依赖
- 通过数据库连接池访问 textures、user_texture_favorites、texture_download_logs
- 使用 GORM 的表达式更新计数器,避免并发竞争
- 服务层依赖
- 负责业务规则编排:权限校验、去重、状态机、计数器更新、日志记录
- 数据库层
- 外键约束textures.uploader_id -> user.idfavorites 与 logs 对 textures/user 的引用
- 索引textures 上的多列索引与计数器降序索引favorites/logs 上的单列索引
```mermaid
graph LR
Service["TextureService"] --> Repo["TextureRepository"]
Repo --> Model["Model: Texture/User/Favorite/DownloadLog"]
Repo --> DB["PostgreSQL 表: textures/favorites/logs"]
Model --> DB
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L110)
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L110)
## 性能考虑
- 并发计数器更新
- 下载计数与收藏计数均使用 GORM 表达式更新,避免读取-计算-写回的竞态,降低锁竞争
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
- 索引设计
- textures 表的多列索引is_public, type, status与计数器降序索引download_count/favorite_count支撑高频查询与排序
- favorites/logs 表的关键列建立索引,提升收藏去重与日志检索效率
- 参考路径:[scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L85)
- 分页与总数
- 搜索与收藏列表先 Count 再分页查询,避免一次性加载大量数据
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- 预加载
- 查询时预加载上传者信息,减少 N+1 查询风险
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L18-L21)
章节来源
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L85)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 故障排查指南
- 材质不存在或已被删除
- 查询时若返回空或状态为已删除,服务层会抛出明确错误
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
- 权限不足
- 更新/删除材质需校验上传者身份,否则返回权限错误
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L120)
- 哈希重复
- 创建材质前检查哈希唯一性,重复则拒绝创建
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31)
- 收藏去重失败
- 联合唯一索引 uk_user_texture 保证同一用户对同一材质仅一条收藏记录
- 若出现重复插入,需检查业务层是否正确先查询后插入
- 参考路径:[internal/model/texture.go](file://internal/model/texture.go#L42-L57)[scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
- 下载计数不准确
- 确认使用表达式更新而非普通更新,避免并发写入导致计数偏差
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L120)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31)
- [internal/model/texture.go](file://internal/model/texture.go#L42-L57)
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
## 结论
材质模型通过清晰的数据结构、严格的外键与索引设计、以及基于 GORM 的并发安全更新,实现了高性能、可扩展且易维护的材质管理能力。状态机与公开性控制使内容治理更加灵活,收藏与下载日志完善了用户行为追踪与运营分析基础。建议在后续迭代中持续关注索引命中率与查询计划,配合缓存与异步任务进一步优化热点查询与批量操作。
## 附录
- 字段与索引对照
- texturesuploader_id、is_public+type+status、download_count、favorite_count、hash 唯一索引
- user_texture_favoritesuser_id、texture_id、created_at、uk_user_texture
- texture_download_logstexture_id、user_id、ip_address、created_at
- 关键查询模式
- 搜索与分页:先 Count 再分页,支持关键词、类型、公开性筛选
- 收藏列表:子查询 + 分页 + 预加载
- 并发计数:表达式更新,避免竞态
章节来源
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L110)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)

View File

@@ -1,473 +0,0 @@
# 档案模型
<cite>
**本文引用的文件**
- [internal/model/profile.go](file://internal/model/profile.go)
- [internal/service/profile_service.go](file://internal/service/profile_service.go)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go)
- [internal/model/texture.go](file://internal/model/texture.go)
- [internal/model/user.go](file://internal/model/user.go)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go)
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
- [internal/types/common.go](file://internal/types/common.go)
- [internal/service/profile_service_test.go](file://internal/service/profile_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件围绕Minecraft档案模型进行系统化技术文档整理重点覆盖以下主题
- Profile结构体的核心字段UUID、Name、SkinID、CapeID以及RSA密钥对的安全存储机制
- 档案激活状态IsActive与最后使用时间LastUsedAt的业务意义
- 与用户User、皮肤Texture的外键关联关系及其在Yggdrasil协议中的作用
- ProfileResponse响应结构的设计原理包括Textures数据的嵌套格式与元数据metadata中模型类型slim/classic的表示方式
- UUID命名规范、角色名唯一性约束以及密钥轮换策略的技术说明并结合实际API响应示例进行说明
## 项目结构
本项目采用分层架构,档案模型位于内部模型层,服务层负责业务流程编排,仓储层负责数据持久化,类型定义用于请求/响应契约与校验。
```mermaid
graph TB
subgraph "模型层"
M_Profile["Profile<br/>profiles 表"]
M_Texture["Texture<br/>textures 表"]
M_User["User<br/>user 表"]
M_Ygg["Yggdrasil<br/>Yggdrasil 表"]
end
subgraph "服务层"
S_Profile["ProfileService<br/>档案业务逻辑"]
S_Ygg["YggdrasilService<br/>Yggdrasil协议集成"]
end
subgraph "仓储层"
R_Profile["ProfileRepository<br/>档案数据访问"]
end
M_Profile --> M_User
M_Profile --> M_Texture
S_Profile --> R_Profile
S_Ygg --> R_Profile
S_Ygg --> M_Profile
S_Ygg --> M_Ygg
```
图表来源
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
章节来源
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
## 核心组件
- ProfileMinecraft档案实体包含UUID、角色名、皮肤/披风ID、RSA私钥、激活状态、最后使用时间等字段并与User、Texture建立关联。
- ProfileResponse对外响应结构包含UUID、角色名、Textures含皮肤/披风URL与metadata模型类型、IsActive、LastUsedAt、CreatedAt。
- KeyPair密钥对结构包含私钥、公钥、过期时间、刷新时间用于安全存储与轮换。
- Texture材质实体支持皮肤/披风类型、URL、哈希、是否公开、下载/收藏计数、是否slim等属性。
- Yggdrasil与用户绑定的Yggdrasil密码实体用于协议认证与会话管理。
章节来源
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
## 架构总览
档案模型贯穿“模型-服务-仓储-外部协议”的完整链路。服务层负责创建/更新/删除档案、设置活跃档案、更新最后使用时间、生成RSA密钥对仓储层负责数据库读写与事务控制模型层定义表结构与关联类型层定义请求/响应契约Yggdrasil服务层负责与外部协议交互如会话数据存储与校验
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "处理器"
participant Service as "ProfileService"
participant Repo as "ProfileRepository"
participant DB as "数据库"
participant Ygg as "YggdrasilService"
Client->>Handler : "创建档案/更新档案/设置活跃档案"
Handler->>Service : "调用业务方法"
Service->>Repo : "查询/更新/事务"
Repo->>DB : "执行SQL/GORM操作"
DB-->>Repo : "返回结果"
Repo-->>Service : "返回实体/影响行数"
Service-->>Handler : "返回业务结果"
Handler-->>Client : "返回ProfileResponse/错误"
Note over Service,DB : "设置活跃档案时,服务层会调用仓储层更新最后使用时间"
Service->>Ygg : "JoinServer/HasJoinedServer与Yggdrasil协议交互"
```
图表来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
## 详细组件分析
### Profile结构体与外键关联
- 字段说明
- UUID档案唯一标识主键长度为36标准UUID格式
- Name角色名最大16字符全局唯一索引
- SkinID/CapeID指向Texture表的外键允许为空
- RSAPrivateKeyRSA私钥PEM格式不返回给前端
- IsActive是否为当前活跃档案默认true带索引
- LastUsedAt最后使用时间用于统计与排序
- CreatedAt/UpdatedAt记录创建与更新时间戳
- 关联关系
- Profile.UserID -> User.ID
- Profile.SkinID -> Texture.ID
- Profile.CapeID -> Texture.ID
```mermaid
classDiagram
class Profile {
+string UUID
+int64 UserID
+string Name
+int64* SkinID
+int64* CapeID
+string RSAPrivateKey
+bool IsActive
+time.Time* LastUsedAt
+time.Time CreatedAt
+time.Time UpdatedAt
}
class User {
+int64 ID
+string Username
+string Email
+string Role
+int16 Status
+time.Time* LastLoginAt
+time.Time CreatedAt
+time.Time UpdatedAt
}
class Texture {
+int64 ID
+int64 UploaderID
+string Name
+string URL
+string Hash
+bool IsPublic
+int DownloadCount
+int FavoriteCount
+bool IsSlim
+int16 Status
+time.Time CreatedAt
+time.Time UpdatedAt
}
Profile --> User : "外键 UserID"
Profile --> Texture : "外键 SkinID"
Profile --> Texture : "外键 CapeID"
```
图表来源
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
章节来源
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
### ProfileResponse响应结构设计
- 结构组成
- uuid/name/is_active/last_used_at/created_at基础档案信息
- textures包含皮肤与披风两个子项
- SKIN/CAPE每个项包含url与metadata
- metadata.model取值为"slim"或"classic",用于指示模型类型
- 设计原则
- 以Yggdrasil协议兼容为目标textures字段直接映射皮肤/披风资源与元数据
- 通过枚举化的model字段明确区分Alex细臂与Steve粗臂模型
```mermaid
classDiagram
class ProfileResponse {
+string uuid
+string name
+ProfileTexturesData textures
+bool is_active
+time.Time* last_used_at
+time.Time created_at
}
class ProfileTexturesData {
+ProfileTexture* SKIN
+ProfileTexture* CAPE
}
class ProfileTexture {
+string url
+ProfileTextureMetadata* metadata
}
class ProfileTextureMetadata {
+string model
}
ProfileResponse --> ProfileTexturesData
ProfileTexturesData --> ProfileTexture
ProfileTexture --> ProfileTextureMetadata
```
图表来源
- [internal/model/profile.go](file://internal/model/profile.go#L31-L64)
章节来源
- [internal/model/profile.go](file://internal/model/profile.go#L31-L64)
### RSA密钥对的安全存储机制
- 生成与存储
- 服务层在创建档案时生成RSA-2048私钥PEM格式并保存至Profile.RSAPrivateKey字段
- 私钥不返回给前端,避免泄露风险
- 读取与轮换
- 仓储层提供GetProfileKeyPair/UpdateProfileKeyPair接口支持从数据库读取与更新密钥对
- KeyPair结构体包含私钥、公钥、过期时间、刷新时间便于后续密钥轮换策略落地
- 安全建议
- 建议在密钥过期前主动轮换,更新数据库中的密钥对并同步到缓存/内存
- 对敏感字段进行最小暴露,仅在必要时解密或传输
```mermaid
flowchart TD
Start(["开始"]) --> Gen["生成RSA-2048私钥PEM"]
Gen --> Save["保存至Profile.RSAPrivateKey"]
Save --> Use["对外响应不返回私钥"]
Use --> Rotate{"是否需要轮换?"}
Rotate --> |否| End(["结束"])
Rotate --> |是| Update["更新数据库中的密钥对"]
Update --> End
```
图表来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L204-L220)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L139-L199)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
章节来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L204-L220)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L139-L199)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
### 档案激活状态与最后使用时间的业务意义
- IsActive
- 用于标记当前用户所选中的活跃档案,同一用户下仅有一个档案处于活跃状态
- 设置活跃档案时,服务层会将该用户其他档案置为非活跃
- LastUsedAt
- 每当设置活跃档案时,服务层会更新该字段为当前时间
- 用于统计与排序,帮助用户快速识别最近使用的档案
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Service as "ProfileService"
participant Repo as "ProfileRepository"
participant DB as "数据库"
Client->>Service : "设置活跃档案"
Service->>Repo : "将用户其他档案置为非活跃"
Repo->>DB : "UPDATE profiles SET is_active=false WHERE user_id=?"
Service->>Repo : "将目标档案置为活跃"
Repo->>DB : "UPDATE profiles SET is_active=true WHERE uuid=? AND user_id=?"
Service->>Repo : "更新最后使用时间"
Repo->>DB : "UPDATE profiles SET last_used_at=CURRENT_TIMESTAMP WHERE uuid=?"
DB-->>Repo : "返回影响行数"
Repo-->>Service : "返回成功"
Service-->>Client : "返回成功"
```
图表来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
章节来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
### 与用户User与皮肤Texture的外键关联关系
- Profile.UserID -> User.ID
- 一对多:一个用户可拥有多个档案
- Profile.SkinID/CapeID -> Texture.ID
- 多对一:一个档案可关联到一张皮肤与一张披风(均可为空)
```mermaid
erDiagram
USER {
int64 id PK
string username UK
string email UK
string role
int16 status
timestamp last_login_at
timestamp created_at
timestamp updated_at
}
TEXTURE {
int64 id PK
int64 uploader_id FK
string name
string url
string hash UK
bool is_public
int download_count
int favorite_count
bool is_slim
int16 status
timestamp created_at
timestamp updated_at
}
PROFILE {
string uuid PK
int64 user_id FK
string name UK
int64* skin_id FK
int64* cape_id FK
text rsa_private_key
bool is_active
timestamp last_used_at
timestamp created_at
timestamp updated_at
}
USER ||--o{ PROFILE : "拥有"
TEXTURE ||--o{ PROFILE : "被使用"
```
图表来源
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
章节来源
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
### 在Yggdrasil协议中的作用
- Profile与Yggdrasil的关系
- ProfileResponse中的textures字段用于向客户端提供皮肤/披风资源与模型元数据满足Yggdrasil协议对纹理与模型类型的要求
- Yggdrasil实体与User存在一对一关联用于协议认证与会话管理
- 会话与验证
- 服务层提供JoinServer/HasJoinedServer方法将会话数据写入Redis并进行用户名/IP校验确保玩家加入服务器的合法性
- 会话数据包含accessToken、userName、selectedProfile、ip等关键字段
```mermaid
sequenceDiagram
participant Client as "客户端"
participant YggSvc as "YggdrasilService"
participant TokenRepo as "TokenRepository"
participant ProfRepo as "ProfileRepository"
participant Redis as "Redis"
Client->>YggSvc : "JoinServer(serverId, accessToken, selectedProfile, ip)"
YggSvc->>TokenRepo : "根据accessToken查询Token"
TokenRepo-->>YggSvc : "返回Token"
YggSvc->>ProfRepo : "根据ProfileId查询Profile"
ProfRepo-->>YggSvc : "返回Profile"
YggSvc->>Redis : "写入会话数据Join_前缀+serverId"
Redis-->>YggSvc : "成功"
YggSvc-->>Client : "返回成功"
```
图表来源
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
章节来源
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
### UUID命名规范、角色名唯一性约束与密钥轮换策略
- UUID命名规范
- Profile.UUID为主键采用标准36字符格式包含连字符
- 服务层在创建档案时使用标准库生成新UUID
- 角色名唯一性约束
- Profile.Name具有唯一索引服务层在创建/更新时均进行冲突检测
- 请求/响应契约中对名称长度有严格限制1-16字符
- 密钥轮换策略
- KeyPair结构体提供过期时间与刷新时间字段便于实现周期性轮换
- 建议在过期前主动生成新密钥对并更新数据库,同时同步到缓存/内存,确保服务可用性
章节来源
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/types/common.go](file://internal/types/common.go#L181-L206)
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L18-L69)
- [internal/service/profile_service_test.go](file://internal/service/profile_service_test.go#L348-L406)
## 依赖分析
- 组件耦合
- ProfileService高度依赖ProfileRepository与数据库/GORM
- ProfileRepository对数据库连接与事务有直接依赖
- YggdrasilService依赖Redis与TokenRepository间接依赖ProfileRepository
- 关联关系
- Profile与User/Texture通过GORM外键注解建立关联
- ProfileResponse与Profile/Texture的嵌套结构映射清晰便于序列化/反序列化
```mermaid
graph LR
ProfileService --> ProfileRepository
ProfileRepository --> Database["GORM/PostgreSQL"]
YggdrasilService --> Redis["Redis"]
YggdrasilService --> ProfileRepository
Profile --> User
Profile --> Texture
```
图表来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
章节来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
## 性能考虑
- 索引优化
- Profile.Name具备唯一索引减少重复角色名查询成本
- Profile.UserID与IsActive具备索引提升活跃档案切换与查询效率
- 预加载策略
- 仓储层在查询档案时预加载Skin/Cape关联避免N+1查询
- 事务与一致性
- 设置活跃档案采用事务,确保原子性与一致性
- 缓存与会话
- Yggdrasil会话数据写入Redis降低频繁查询数据库的压力
## 故障排查指南
- 常见错误与定位
- 角色名冲突:创建/更新时若返回“角色名已被使用”检查Profile.Name唯一性约束与服务层校验逻辑
- 权限不足操作他人档案会返回“无权操作此档案”检查服务层对Profile.UserID与传入userID的比对
- 档案不存在:查询/更新/删除时若返回“档案不存在”检查UUID格式与仓储层查询条件
- 密钥读取失败GetProfileKeyPair返回“未找到”或错误检查数据库字段映射与查询条件
- 调试建议
- 在服务层与仓储层增加日志输出,定位具体环节(查询、更新、事务)
- 使用单元测试验证请求/响应契约与边界条件名称长度、UUID格式、密钥PEM格式
章节来源
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L71-L159)
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L139-L199)
- [internal/service/profile_service_test.go](file://internal/service/profile_service_test.go#L348-L406)
## 结论
本档案模型围绕Profile为核心结合ProfileResponse的纹理与元数据设计满足Minecraft Yggdrasil协议对皮肤/披风与模型类型的要求。通过严格的唯一性约束、索引优化与事务保障确保了数据一致性与性能。RSA密钥对的安全存储与KeyPair结构为后续密钥轮换提供了基础。与User/Texture的外键关联清晰地刻画了用户与材质的使用关系配合Yggdrasil服务层的会话管理形成完整的档案生命周期闭环。
## 附录
- API响应示例概念性说明
- ProfileResponse示例包含uuid、name、textures含SKIN/CAPE、is_active、last_used_at、created_at等字段
- textures.metadata.model取值为"slim"或"classic",用于指示模型类型
- 请求/响应契约要点
- UpdateProfileRequest对name长度与skin_id/cape_id进行约束
- CreateProfileRequest对角色名长度与类型进行约束
章节来源
- [internal/model/profile.go](file://internal/model/profile.go#L31-L64)
- [internal/types/common.go](file://internal/types/common.go#L181-L206)

View File

@@ -1,451 +0,0 @@
# 用户模型
<cite>
**本文引用的文件**
- [internal/model/user.go](file://internal/model/user.go)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
- [internal/service/user_service.go](file://internal/service/user_service.go)
- [pkg/auth/password.go](file://pkg/auth/password.go)
- [pkg/database/postgres.go](file://pkg/database/postgres.go)
- [internal/model/profile.go](file://internal/model/profile.go)
- [internal/model/system_config.go](file://internal/model/system_config.go)
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go)
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go)
- [internal/repository/user_repository_test.go](file://internal/repository/user_repository_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件聚焦于用户模型 User 的字段定义、数据类型、GORM 标签与业务含义,以及与 UserPointLog、UserLoginLog 的关联关系。同时提供用户状态流转图、积分系统使用模式、属性扩展Properties JSONB的实践建议并给出数据验证规则、唯一性约束与索引优化建议最后结合代码路径说明如何安全地处理密码存储与敏感信息脱敏。
## 项目结构
围绕用户模型的关键文件分布如下:
- 模型层:用户、积分日志、登录日志、档案等
- 仓储层:用户 CRUD、积分更新、登录日志创建等
- 服务层:注册、登录、修改密码、更换邮箱等业务流程
- 安全层:密码哈希与校验
- 数据库层PostgreSQL 连接与配置
```mermaid
graph TB
subgraph "模型层"
M_User["User<br/>internal/model/user.go"]
M_PointLog["UserPointLog<br/>internal/model/user.go"]
M_LoginLog["UserLoginLog<br/>internal/model/user.go"]
M_Profile["Profile<br/>internal/model/profile.go"]
end
subgraph "仓储层"
R_UserRepo["UserRepository<br/>internal/repository/user_repository.go"]
end
subgraph "服务层"
S_UserSvc["UserService<br/>internal/service/user_service.go"]
S_Serialize["SerializeService<br/>internal/service/serialize_service.go"]
end
subgraph "安全层"
A_Password["Password Hash/Check<br/>pkg/auth/password.go"]
end
subgraph "数据库层"
D_Postgres["PostgreSQL连接<br/>pkg/database/postgres.go"]
end
S_UserSvc --> R_UserRepo
R_UserRepo --> D_Postgres
S_UserSvc --> A_Password
S_Serialize --> M_User
M_User --> M_PointLog
M_User --> M_LoginLog
M_Profile --> M_User
```
图表来源
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
章节来源
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
## 核心组件
本节对 User 结构体及其关键字段进行逐项解析涵盖字段类型、GORM 标签、默认值、业务语义与约束。
- 字段与类型
- ID整型自增主键用于唯一标识用户
- Username字符串长度限制与唯一索引约束
- Password字符串存储哈希后的密码JSON 中不返回
- Email字符串长度限制与唯一索引约束
- Avatar字符串头像 URL默认空串
- Points整型积分余额默认 0
- Role字符串角色默认 "user"
- Status短整型状态枚举默认 1正常0禁用-1删除
- Properties字符串映射 PostgreSQL 的 JSONB 类型,用于扩展属性
- LastLoginAt时间戳最近登录时间
- CreatedAt/UpdatedAt时间戳默认 CURRENT_TIMESTAMP
- 约束与索引
- Username、Email 唯一索引
- UserPointLog、UserLoginLog 的 UserID、IP 地址、is_success 等字段建立索引
- Properties 以 JSONB 存储,便于灵活扩展
- 业务含义
- 注册时默认 Role 为 "user"Status 为 1正常Points 为 0
- 登录成功后更新 LastLoginAt
- 删除采用软删除,将 Status 设为 -1
章节来源
- [internal/model/user.go](file://internal/model/user.go#L1-L26)
- [internal/model/user.go](file://internal/model/user.go#L28-L70)
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
- [internal/service/user_service.go](file://internal/service/user_service.go#L111-L121)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L71-L75)
## 架构总览
用户模型在系统中的交互流程如下:
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "处理器/控制器"
participant Service as "UserService"
participant Repo as "UserRepository"
participant DB as "数据库(PostgreSQL)"
participant Auth as "Password(哈希/校验)"
Client->>Handler : "注册/登录请求"
Handler->>Service : "调用业务方法"
Service->>Auth : "注册时哈希密码"
Auth-->>Service : "返回哈希值"
Service->>Repo : "创建用户/保存用户"
Repo->>DB : "执行SQL"
DB-->>Repo : "返回结果"
Repo-->>Service : "返回用户对象"
Service-->>Handler : "返回用户与令牌"
Handler-->>Client : "响应(不含敏感字段)"
```
图表来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L11-L15)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
## 详细组件分析
### User 结构体与字段详解
- ID
- 类型:整型自增主键
- 用途:全局唯一标识
- GORM 标签:主键、自增
- Username
- 类型:字符串,长度限制,唯一索引
- 用途:登录凭据之一
- 约束:唯一性
- Password
- 类型:字符串,存储 bcrypt 哈希
- 用途:登录凭据
- 安全JSON 中不返回
- Email
- 类型:字符串,长度限制,唯一索引
- 用途:找回密码、通知等
- 约束:唯一性
- Avatar
- 类型字符串URL
- 默认:空串
- Points
- 类型:整型
- 默认0
- 用途:积分系统余额
- Role
- 类型:字符串
- 默认:"user"
- Status
- 类型:短整型
- 默认1正常
- 取值1 正常0 禁用;-1 删除(软删)
- Properties
- 类型:字符串,映射 PostgreSQL 的 JSONB
- 用途:扩展属性,如权限、配置等
- LastLoginAt
- 类型:时间戳
- 用途:记录最近登录时间
- CreatedAt/UpdatedAt
- 类型:时间戳,默认 CURRENT_TIMESTAMP
章节来源
- [internal/model/user.go](file://internal/model/user.go#L1-L26)
### User 与 UserPointLog 的关联
- 外键关系
- UserPointLog.UserID 引用 User.ID
- UserPointLog.OperatorID 可选,指向操作者用户
- 级联行为
- 代码中未显式声明级联删除/更新,遵循 GORM 默认行为
- 由于 User 采用软删除Status=-1建议在业务层避免直接删除用户防止破坏积分日志的完整性
- 日志字段
- ChangeType变更类型如 EARN、SPEND、ADMIN_ADJUST
- Amount/BalanceBefore/BalanceAfter金额与前后余额
- Reason/ReferenceType/ReferenceID原因与关联对象类型/ID
- OperatorID可选记录管理员操作者
```mermaid
classDiagram
class User {
+int64 ID
+string Username
+string Email
+string Avatar
+int Points
+string Role
+int16 Status
+string Properties
+time.Time LastLoginAt
+time.Time CreatedAt
+time.Time UpdatedAt
}
class UserPointLog {
+int64 ID
+int64 UserID
+string ChangeType
+int Amount
+int BalanceBefore
+int BalanceAfter
+string Reason
+string ReferenceType
+*int64 ReferenceID
+*int64 OperatorID
+time.Time CreatedAt
}
class UserLoginLog {
+int64 ID
+int64 UserID
+string IPAddress
+string UserAgent
+string LoginMethod
+bool IsSuccess
+string FailureReason
+time.Time CreatedAt
}
User "1" <-- "many" UserPointLog : "外键UserID"
User "1" <-- "many" UserLoginLog : "外键UserID"
User "1" <-- "many" UserPointLog : "外键OperatorID(可选)"
```
图表来源
- [internal/model/user.go](file://internal/model/user.go#L28-L70)
章节来源
- [internal/model/user.go](file://internal/model/user.go#L28-L70)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
### User 与 UserLoginLog 的关联
- 外键关系
- UserLoginLog.UserID 引用 User.ID
- 日志字段
- IPAddressinet 类型,记录登录 IP
- UserAgent文本
- LoginMethod默认 PASSWORD
- IsSuccess/FailureReason登录成功与否及原因
- 索引
- UserID、IPAddress、IsSuccess 建有索引CreatedAt 建有复合索引并按降序排序
章节来源
- [internal/model/user.go](file://internal/model/user.go#L52-L70)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L77-L87)
### 用户状态流转图
- 正常1可登录、可消费积分
- 禁用0禁止登录不影响积分
- 删除(-1软删除查询时默认过滤该状态
```mermaid
stateDiagram-v2
[*] --> 正常
正常 --> 禁用 : "管理员操作"
正常 --> 删除 : "软删除"
禁用 --> 正常 : "恢复"
禁用 --> 删除 : "软删除"
删除 --> 正常 : "不可恢复"
删除 --> 禁用 : "不可恢复"
```
图表来源
- [internal/model/user.go](file://internal/model/user.go#L14-L16)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L71-L75)
### 积分系统Points使用模式
- 初始化:注册时 Points 默认 0
- 更新:通过 UpdateUserPoints 在事务中完成,先读取当前余额,计算新余额,再写入并创建日志
- 校验:若新余额小于 0则回滚并返回错误
- 日志:记录变更类型、金额、前后余额、原因、关联对象与操作者
```mermaid
flowchart TD
Start(["开始"]) --> ReadUser["读取用户当前积分"]
ReadUser --> Calc["计算新余额 = 当前 + 变更金额"]
Calc --> Check{"新余额 >= 0 ?"}
Check --> |否| Rollback["回滚事务并返回错误"]
Check --> |是| Update["更新用户积分"]
Update --> Log["创建积分日志"]
Log --> End(["结束"])
Rollback --> End
```
图表来源
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
章节来源
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
### 属性扩展Properties JSONB使用模式
- 存储Properties 为字符串,映射 PostgreSQL 的 JSONB 类型
- 用途:存放扩展字段,如权限、配置等
- 序列化:在序列化时可直接返回 Properties但需注意敏感信息脱敏
- 示例路径:序列化用户时将 Properties 原样返回,供上层处理
章节来源
- [internal/model/user.go](file://internal/model/user.go#L16-L17)
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
### 数据验证规则、唯一性约束与索引优化建议
- 验证规则
- 用户名非空,长度范围(建议在服务层补充)
- 邮箱非空且包含 @ 符号(建议在服务层补充)
- 密码非空(建议在服务层补充)
- 唯一性约束
- Username、Email 唯一索引
- 索引优化建议
- UserUsername、Email 唯一索引(已具备)
- UserPointLogUserID、CreatedAt(降序)、ReferenceType/ReferenceID可选
- UserLoginLogUserID、IPAddress、IsSuccess、CreatedAt(降序)
- 状态查询
- 查询用户时默认排除 Status=-1软删除
章节来源
- [internal/model/user.go](file://internal/model/user.go#L9-L21)
- [internal/model/user.go](file://internal/model/user.go#L31-L41)
- [internal/model/user.go](file://internal/model/user.go#L55-L61)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L18-L29)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L31-L43)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L45-L57)
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go#L109-L164)
- [internal/repository/user_repository_test.go](file://internal/repository/user_repository_test.go#L1-L69)
### 密码存储与敏感信息脱敏
- 密码存储
- 注册时使用 bcrypt 哈希存储
- 登录时使用 bcrypt 校验
- User.Password 在 JSON 响应中不返回
- 敏感信息脱敏
- Password 字段在 JSON 中标记为不输出
- Properties 作为 JSONB 返回,需在上层进行脱敏处理(例如移除敏感键或替换为占位符)
- 头像 URL、邮箱等字段在返回前可根据需要进行脱敏策略
章节来源
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
- [internal/service/user_service.go](file://internal/service/user_service.go#L141-L164)
- [internal/model/user.go](file://internal/model/user.go#L10-L12)
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
## 依赖分析
- 组件耦合
- UserService 依赖 UserRepository、JWT 服务与 Password 工具
- UserRepository 依赖数据库连接
- User 模型与 UserPointLog、UserLoginLog 通过外键关联
- 外键与级联
- 未显式声明级联,遵循 GORM 默认行为
- 建议在业务层避免直接物理删除用户,以保持积分与登录日志的完整性
- 外部依赖
- PostgreSQL 驱动与连接池配置
- bcrypt 密码哈希
```mermaid
graph LR
S_UserSvc["UserService"] --> R_UserRepo["UserRepository"]
S_UserSvc --> A_Password["Password"]
R_UserRepo --> D_Postgres["PostgreSQL"]
M_User["User"] --> M_PointLog["UserPointLog"]
M_User --> M_LoginLog["UserLoginLog"]
```
图表来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
## 性能考虑
- 索引设计
- UserUsername、Email 唯一索引
- UserPointLogUserID、CreatedAt(降序)、ReferenceType/ReferenceID可选
- UserLoginLogUserID、IPAddress、IsSuccess、CreatedAt(降序)
- 查询过滤
- 查询用户时默认排除软删除Status!=-1减少扫描
- 事务与并发
- 积分更新使用事务,保证一致性
- 缓存与限流
- 登录失败与成功日志可配合缓存与限流策略,降低暴力破解风险
[本节为通用指导,无需列出具体文件来源]
## 故障排查指南
- 登录失败
- 检查用户是否存在与状态是否为 1
- 校验密码哈希是否正确
- 记录失败日志以便审计
- 积分不足
- 确认余额计算逻辑与事务一致性
- 检查日志是否正确记录
- 软删除影响
- 查询用户时默认排除 Status=-1
- 若出现“用户不存在”,确认是否被软删除
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L70-L121)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L71-L75)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
- [internal/repository/user_repository_test.go](file://internal/repository/user_repository_test.go#L1-L69)
## 结论
User 模型通过明确的字段定义、GORM 标签与业务约束,支撑了注册、登录、积分与日志等核心功能。通过软删除与事务化的积分更新,确保了数据一致性与可审计性。建议在服务层补充输入验证与脱敏策略,并根据业务增长持续优化索引与查询路径。
[本节为总结性内容,无需列出具体文件来源]
## 附录
- 相关模型与配置
- Profile 模型与 User 的关联
- SystemConfig 模型(系统配置)
- 序列化与脱敏
- SerializeUser 返回 Properties需在上层进行脱敏处理
章节来源
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)

View File

@@ -1,369 +0,0 @@
# 系统配置模型
<cite>
**本文引用的文件**
- [internal/model/system_config.go](file://internal/model/system_config.go)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go)
- [internal/types/common.go](file://internal/types/common.go)
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/config/manager.go](file://pkg/config/manager.go)
- [scripts/check-env.sh](file://scripts/check-env.sh)
</cite>
## 目录
1. [引言](#引言)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 引言
本文件围绕系统配置模型进行系统性说明,重点聚焦于 SystemConfig 结构体的设计与应用,涵盖:
- 键值配置存储机制与配置类型ConfigTypeSTRING/INTEGER/BOOLEAN/JSON的枚举定义与值序列化策略
- IsPublic 标志位如何控制前端可访问的配置项(站点名称、注册开关、维护模式等)
- SystemConfigPublicResponse 响应结构的字段映射逻辑与敏感配置与公开配置的分离原则
- 关于配置项版本管理、变更审计与缓存策略的建议
- 如何安全地读取与更新系统参数,避免配置注入风险
## 项目结构
系统配置模型位于内部模型层与仓库层,配合通用类型与配置加载模块共同构成完整的配置体系。下图展示了与系统配置模型直接相关的文件与职责划分。
```mermaid
graph TB
subgraph "模型层"
M1["internal/model/system_config.go<br/>定义 SystemConfig 与 SystemConfigPublicResponse"]
end
subgraph "仓库层"
R1["internal/repository/system_config_repository.go<br/>提供配置查询与更新接口"]
R2["internal/repository/system_config_repository_test.go<br/>测试查询条件、公开配置逻辑与更新逻辑"]
end
subgraph "类型与响应"
T1["internal/types/common.go<br/>定义 SystemConfigResponse 等通用响应类型"]
end
subgraph "配置加载"
C1["pkg/config/config.go<br/>应用运行时配置加载与环境变量覆盖"]
C2["pkg/config/manager.go<br/>全局配置实例管理"]
S1["scripts/check-env.sh<br/>环境变量检查脚本"]
end
M1 --> R1
R1 --> C1
T1 -.-> M1
C1 --> C2
S1 --> C1
```
图表来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L1-L145)
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L61)
章节来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L61)
## 核心组件
- SystemConfig系统配置的持久化模型包含键、值、类型、描述、是否公开、创建与更新时间等字段通过 GORM 映射到 system_config 表。
- ConfigType配置类型枚举支持 STRING、INTEGER、BOOLEAN、JSON 四种类型。
- IsPublic布尔标志位决定该配置是否可被前端获取。
- SystemConfigPublicResponse面向前端的公开配置响应结构包含站点名称、站点描述、注册开关、维护模式、公告等字段。
- SystemConfigRepository提供按键查询、获取公开配置、获取全部配置、更新配置与更新配置值等操作。
- SystemConfigResponse通用系统配置响应类型与公开响应不同包含更多业务参数
章节来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
## 架构总览
系统配置模型遵循“模型-仓库-服务-控制器”的分层设计。配置读写通过仓库层封装数据库访问,对外暴露简洁的接口;公开配置通过 IsPublic 进行过滤,确保敏感信息不泄露给前端。
```mermaid
graph TB
Client["前端/管理端"] --> API["控制器/服务层"]
API --> Repo["SystemConfigRepository"]
Repo --> Model["SystemConfig 模型"]
Repo --> DB["数据库 system_config 表"]
API --> PublicResp["SystemConfigPublicResponse"]
API --> CommonResp["SystemConfigResponse"]
```
图表来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L18-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
## 详细组件分析
### SystemConfig 结构体与配置类型
- 字段说明
- Key配置键唯一索引用于标识具体配置项。
- Value配置值以字符串形式存储配合 Type 决定解析方式。
- Type配置类型枚举 STRING/INTEGER/BOOLEAN/JSON。
- IsPublic是否公开true 时可在公开接口中返回给前端。
- Description、CreatedAt、UpdatedAt元信息与时间戳。
- 表名映射TableName 返回 system_config用于 GORM 自动迁移与查询。
- 配置类型与序列化策略
- STRING直接以字符串存储适合文本类配置。
- INTEGER字符串存储需要在读取后转换为整数。
- BOOLEAN字符串存储需要在读取后转换为布尔值。
- JSON字符串存储需要在读取后进行 JSON 解析,支持复杂结构。
```mermaid
classDiagram
class SystemConfig {
+int64 id
+string key
+string value
+string description
+ConfigType type
+bool is_public
+time created_at
+time updated_at
+TableName() string
}
class ConfigType {
<<enumeration>>
"STRING"
"INTEGER"
"BOOLEAN"
"JSON"
}
SystemConfig --> ConfigType : "使用"
```
图表来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L32)
章节来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L32)
### 公开配置响应 SystemConfigPublicResponse 的字段映射
- 字段含义
- site_name站点名称
- site_description站点描述
- registration_enabled注册开关
- maintenance_mode维护模式
- announcement公告
- 映射原则
- 仅来源于 IsPublic 为 true 的配置项,避免敏感配置泄露。
- 字段名与 JSON 序列化标签一一对应,便于前端消费。
- 分离原则
- 敏感配置(如密钥、内部开关)应设置 IsPublic=false不在公开响应中出现。
- 公开配置(如站点名称、注册开关、维护模式、公告)应设置 IsPublic=true。
```mermaid
classDiagram
class SystemConfigPublicResponse {
+string site_name
+string site_description
+bool registration_enabled
+bool maintenance_mode
+string announcement
}
```
图表来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L34-L41)
章节来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L34-L41)
### 仓库层接口与流程
- GetSystemConfigByKey(key):根据键查询配置,若记录不存在返回空而非错误。
- GetPublicSystemConfigs():仅返回 IsPublic=true 的配置集合。
- GetAllSystemConfigs():返回全部配置(供管理员使用)。
- UpdateSystemConfig(config):保存配置(含类型与值)。
- UpdateSystemConfigValue(key, value):仅更新值字段。
```mermaid
sequenceDiagram
participant Client as "调用方"
participant Repo as "SystemConfigRepository"
participant DB as "数据库"
Client->>Repo : GetPublicSystemConfigs()
Repo->>DB : 查询 where is_public = true
DB-->>Repo : 返回公开配置列表
Repo-->>Client : 返回公开配置
Client->>Repo : UpdateSystemConfigValue(key, value)
Repo->>DB : Model(...).Where("key = ?").Update("value", value)
DB-->>Repo : 更新成功
Repo-->>Client : 返回 nil
```
图表来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L57)
章节来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
### 配置读取与序列化策略
- 读取流程
- 使用 GetSystemConfigByKey(key) 获取配置。
- 根据 Type 对 Value 进行类型化解析:
- STRING直接使用字符串。
- INTEGER解析为整数。
- BOOLEAN解析为布尔值。
- JSON解析为结构体或 map。
- 序列化策略
- 公开响应使用 SystemConfigPublicResponse仅包含 IsPublic=true 的字段映射。
- 非公开配置不参与公开响应序列化。
```mermaid
flowchart TD
Start(["开始"]) --> Load["GetSystemConfigByKey(key)"]
Load --> Found{"找到记录?"}
Found --> |否| ReturnNil["返回空配置"]
Found --> |是| ParseType["根据 Type 解析 Value"]
ParseType --> STRING["STRING直接使用字符串"]
ParseType --> INTEGER["INTEGER解析为整数"]
ParseType --> BOOLEAN["BOOLEAN解析为布尔值"]
ParseType --> JSON["JSON解析为结构体/字典"]
STRING --> End(["结束"])
INTEGER --> End
BOOLEAN --> End
JSON --> End
ReturnNil --> End
```
图表来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L23)
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L15)
章节来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L23)
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L15)
### 公开配置逻辑与测试验证
- 公开配置查询逻辑
- GetPublicSystemConfigs() 仅返回 IsPublic=true 的配置,确保敏感配置不被泄露。
- 测试覆盖点
- 查询条件验证:键非空才视为有效。
- 公开配置逻辑:仅包含 IsPublic=true 的配置。
- 更新值逻辑:键非空即允许更新,空值亦可接受。
- 错误处理:记录不存在时返回空而非错误。
```mermaid
flowchart TD
QStart(["查询入口"]) --> BuildQuery["构建查询where is_public = true"]
BuildQuery --> Exec["执行查询"]
Exec --> Result{"查询结果"}
Result --> |存在| Filter["过滤 IsPublic=true"]
Result --> |不存在| NotFound["返回空集合"]
Filter --> Return["返回公开配置集合"]
NotFound --> Return
```
图表来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L33)
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L51-L78)
章节来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L33)
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L1-L145)
### 安全与注入防护
- 输入校验
- 键非空校验:更新与查询均要求键非空,防止空键导致的异常行为。
- 值可为空:允许空值,但需结合业务语义判断有效性。
- 类型约束
- 通过 Type 字段强制约束 Value 的解析方式,避免任意字符串被错误解读。
- 敏感信息隔离
- 通过 IsPublic=false 隐藏敏感配置,仅在管理员通道可见。
- 环境变量安全
- 应用配置加载来自环境变量,建议使用强口令与密钥,脚本会提示 JWT 密钥长度不足的风险。
章节来源
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L80-L116)
- [scripts/check-env.sh](file://scripts/check-env.sh#L52-L61)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L305)
## 依赖关系分析
- 模型依赖
- SystemConfig 依赖 GORM 标签进行数据库映射。
- SystemConfigPublicResponse 依赖 JSON 标签进行序列化。
- 仓库依赖
- 依赖数据库连接MustGetDB通过 where 条件实现键查询与公开筛选。
- 类型依赖
- SystemConfigResponse 与 SystemConfigPublicResponse 分别服务于不同场景的响应结构。
- 配置加载依赖
- 应用配置加载与环境变量覆盖,确保运行时参数可控且可审计。
```mermaid
graph LR
Model["SystemConfig 模型"] --> Repo["SystemConfigRepository"]
Repo --> DB["数据库"]
PublicResp["SystemConfigPublicResponse"] --> API["控制器/服务层"]
CommonResp["SystemConfigResponse"] --> API
Cfg["pkg/config/config.go"] --> API
CfgMgr["pkg/config/manager.go"] --> Cfg
```
图表来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L18-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
章节来源
- [internal/model/system_config.go](file://internal/model/system_config.go#L18-L41)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
## 性能考量
- 查询优化
- 为 key 建立唯一索引,提升按键查询效率。
- 为 is_public 建立索引,加速公开配置筛选。
- 缓存策略建议
- 公开配置可引入只读缓存(如 Redis定期刷新降低数据库压力。
- 对频繁读取的配置项(如站点名称、注册开关、维护模式)设置短 TTL平衡一致性与性能。
- 更新策略
- 批量更新时使用 UpdateSystemConfigValue避免不必要的字段变更导致的冗余日志与锁竞争。
- 版本与审计
- 建议引入配置版本号字段与审计日志,记录每次变更的时间、操作者与变更内容,便于回溯与合规。
## 故障排查指南
- 记录不存在
- GetSystemConfigByKey(key) 在记录不存在时返回空而非错误,调用方需显式判空。
- 公开配置为空
- 若 GetPublicSystemConfigs() 返回空,检查是否正确设置 IsPublic=true 或数据库中是否存在公开配置。
- 更新失败
- UpdateSystemConfigValue(key, value) 要求键非空;若更新无效,确认键是否存在且值合法。
- 类型解析错误
- 根据 Type 对 Value 进行解析;若解析失败,检查配置值格式与类型是否匹配。
- 环境变量问题
- 使用脚本检查关键环境变量是否缺失或过短(如 JWT 密钥长度),及时修正。
章节来源
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L23)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L57)
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L119-L145)
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L61)
## 结论
SystemConfig 模型通过统一的键值存储与类型约束,实现了灵活而安全的系统配置管理。借助 IsPublic 标志位与公开响应结构,系统在保证功能可用的同时,严格隔离了敏感配置。结合缓存、版本与审计策略,可进一步提升系统的稳定性与可运维性。建议在生产环境中严格执行输入校验、类型解析与注入防护,并建立完善的变更审计流程。
## 附录
- 常用配置键建议
- 站点名称site_nameIsPublic=true
- 站点描述site_descriptionIsPublic=true
- 注册开关registration_enabledIsPublic=true
- 维护模式maintenance_modeIsPublic=true
- 公告announcementIsPublic=true
- 管理员专用secret_key、internal_switchIsPublic=false

View File

@@ -1,269 +0,0 @@
# 服务架构
<cite>
**本文档引用文件**
- [user_service.go](file://internal/service/user_service.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [user_repository.go](file://internal/repository/user_repository.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [user.go](file://internal/model/user.go)
- [profile.go](file://internal/model/profile.go)
- [texture.go](file://internal/model/texture.go)
</cite>
## 目录
1. [简介](#简介)
2. [服务层职责概述](#服务层职责概述)
3. [核心服务详细分析](#核心服务详细分析)
4. [服务调用关系与流程](#服务调用关系与流程)
5. [业务规则实现](#业务规则实现)
6. [错误处理与日志记录](#错误处理与日志记录)
7. [结论](#结论)
## 简介
CarrotSkin项目的服务层是业务逻辑的核心负责协调数据访问、执行业务规则并为上层处理器提供接口。本服务架构文档重点介绍`UserService``ProfileService``TextureService`三大核心服务详细说明其职责、方法、内部逻辑以及与Repository层的协作方式。文档旨在为初学者提供清晰的服务调用视图并为经验丰富的开发者提供代码扩展和维护的深入指导。
## 服务层职责概述
服务层(位于`internal/service/`目录是CarrotSkin应用的业务逻辑中枢其主要职责包括
- **业务逻辑封装**:将复杂的业务规则(如用户注册、档案创建、材质上传)封装在独立的服务方法中。
- **数据访问协调**调用Repository层的方法来持久化或检索数据实现与数据库的解耦。
- **事务管理**对于涉及多个数据操作的业务如设置活跃档案通过Repository层的事务功能保证数据一致性。
- **数据验证与转换**:在数据进入或离开系统时进行验证和必要的格式转换。
- **外部服务集成**与认证JWT、存储MinIO、邮件等外部服务进行交互。
服务层通过定义清晰的函数接口,为`handler`层提供稳定、可复用的业务能力,是连接用户请求与数据持久化的关键桥梁。
## 核心服务详细分析
### UserService 分析
`UserService`负责管理用户生命周期的核心操作,包括注册、登录、信息更新和密码管理。
**核心方法与职责**
- `RegisterUser`:处理用户注册流程。该方法首先检查用户名和邮箱的唯一性,然后使用`pkg/auth`包对密码进行加密创建用户记录并生成JWT Token。它还实现了头像URL的逻辑优先使用用户提供的URL否则从系统配置中获取默认头像。
- `LoginUser`:处理用户登录。支持通过用户名或邮箱登录。方法会根据输入是否包含`@`符号来判断登录方式。成功登录后,会更新用户的最后登录时间,并记录登录日志(成功或失败)。
- `ChangeUserPassword``ResetUserPassword`:分别处理用户主动修改密码和通过邮箱重置密码的场景。两者都包含密码验证和加密更新的逻辑。
- `UpdateUserInfo``UpdateUserAvatar`:用于更新用户的基本信息和头像。
该服务通过调用`repository.FindUserByUsername``repository.FindUserByEmail`等方法与Repository层交互确保了数据访问的抽象化。
```mermaid
classDiagram
class UserService {
+RegisterUser(jwtService *auth.JWTService, username, password, email, avatar string) (*model.User, string, error)
+LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error)
+GetUserByID(id int64) (*model.User, error)
+UpdateUserInfo(user *model.User) error
+UpdateUserAvatar(userID int64, avatarURL string) error
+ChangeUserPassword(userID int64, oldPassword, newPassword string) error
+ResetUserPassword(email, newPassword string) error
+ChangeUserEmail(userID int64, newEmail string) error
}
class User {
+ID int64
+Username string
+Email string
+Avatar string
+Points int
+Role string
+Status int16
}
class Repository {
+FindUserByUsername(username string) (*model.User, error)
+FindUserByEmail(email string) (*model.User, error)
+CreateUser(user *model.User) error
+UpdateUserFields(id int64, fields map[string]interface{}) error
}
class Auth {
+HashPassword(password string) (string, error)
+CheckPassword(hashedPassword, password string) bool
+GenerateToken(userID int64, username, role string) (string, error)
}
UserService --> User : "操作"
UserService --> Repository : "调用"
UserService --> Auth : "依赖"
```
**图示来源**
- [user_service.go](file://internal/service/user_service.go#L12-L249)
- [user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [user.go](file://internal/model/user.go#L1-L71)
**本节来源**
- [user_service.go](file://internal/service/user_service.go#L12-L249)
- [user_repository.go](file://internal/repository/user_repository.go#L1-L137)
### ProfileService 分析
`ProfileService`负责管理用户的Minecraft档案Profile包括创建、查询、更新、删除和状态管理。
**核心方法与职责**
- `CreateProfile`创建新的Minecraft档案。此方法首先验证用户存在且状态正常检查角色名的唯一性然后生成一个UUID作为档案ID并调用`generateRSAPrivateKey`生成RSA-2048私钥PEM格式。创建档案后会调用`SetActiveProfile`确保该档案成为用户的活跃档案。
- `GetUserProfiles``GetProfileByUUID`:分别用于获取用户的所有档案列表和单个档案的详细信息。
- `UpdateProfile`:更新档案信息,如角色名、皮肤和披风。更新角色名时会检查是否重复。更新后会重新加载数据以返回最新状态。
- `DeleteProfile`:删除指定的档案。
- `SetActiveProfile`:将指定档案设置为活跃状态。这是一个关键的业务方法,它在一个数据库事务中执行:首先将用户的所有档案设置为非活跃,然后将指定的档案设置为活跃,从而保证了同一时间只有一个活跃档案。
- `CheckProfileLimit`:检查用户当前的档案数量是否达到了系统设定的上限,用于在创建新档案前进行限制。
该服务通过`repository.FindProfileByUUID``repository.CreateProfile`等方法与Repository层交互并利用GORM的事务功能来保证`SetActiveProfile`操作的原子性。
```mermaid
classDiagram
class ProfileService {
+CreateProfile(db *gorm.DB, userID int64, name string) (*model.Profile, error)
+GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error)
+GetUserProfiles(db *gorm.DB, userID int64) ([]*model.Profile, error)
+UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error)
+DeleteProfile(db *gorm.DB, uuid string, userID int64) error
+SetActiveProfile(db *gorm.DB, uuid string, userID int64) error
+CheckProfileLimit(db *gorm.DB, userID int64, maxProfiles int) error
}
class Profile {
+UUID string
+UserID int64
+Name string
+SkinID *int64
+CapeID *int64
+RSAPrivateKey string
+IsActive bool
}
class Repository {
+FindProfileByUUID(uuid string) (*model.Profile, error)
+CreateProfile(profile *model.Profile) error
+FindProfilesByUserID(userID int64) ([]*model.Profile, error)
+CountProfilesByUserID(userID int64) (int64, error)
+SetActiveProfile(uuid string, userID int64) error
+DeleteProfile(uuid string) error
}
ProfileService --> Profile : "操作"
ProfileService --> Repository : "调用"
```
**图示来源**
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [profile.go](file://internal/model/profile.go#L1-L64)
**本节来源**
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
### TextureService 分析
`TextureService`负责管理用户上传的皮肤Skin和披风Cape材质。
**核心方法与职责**
- `CreateTexture`创建新的材质记录。方法会验证上传者用户的存在性并检查材质的Hash值是否已存在防止重复上传。它通过`switch`语句将字符串类型的`textureType`转换为`model.TextureType`枚举值。创建成功后,材质记录会被持久化。
- `SearchTextures``GetUserTextures`:提供按关键词、类型、公开性等条件搜索材质,以及获取特定用户上传的材质列表的功能。
- `UpdateTexture`:允许上传者更新材质的名称、描述和公开状态。更新时会检查权限,确保只有上传者才能修改。
- `DeleteTexture`:允许上传者删除自己的材质。
- `RecordTextureDownload`:记录一次材质下载行为。该方法会增加材质的下载计数,并创建一条下载日志。
- `ToggleTextureFavorite`:切换用户对某个材质的收藏状态。如果已收藏则取消收藏并减少收藏计数,反之则添加收藏并增加收藏计数。
- `CheckTextureUploadLimit`:检查用户上传的材质数量是否达到了系统设定的上限。
该服务通过`repository.FindTextureByHash``repository.CreateTexture`等方法与Repository层交互并通过`IncrementTextureDownloadCount``DecrementTextureFavoriteCount`等原子操作来安全地更新计数。
```mermaid
classDiagram
class TextureService {
+CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error)
+GetTextureByID(db *gorm.DB, id int64) (*model.Texture, error)
+GetUserTextures(db *gorm.DB, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
+SearchTextures(db *gorm.DB, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
+UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error)
+DeleteTexture(db *gorm.DB, textureID, uploaderID int64) error
+RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddress, userAgent string) error
+ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error)
+GetUserTextureFavorites(db *gorm.DB, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
+CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) error
}
class Texture {
+ID int64
+UploaderID int64
+Name string
+Type TextureType
+URL string
+Hash string
+DownloadCount int
+FavoriteCount int
}
class Repository {
+FindTextureByHash(hash string) (*model.Texture, error)
+CreateTexture(texture *model.Texture) error
+FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
+SearchTextures(keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
+UpdateTextureFields(id int64, fields map[string]interface{}) error
+DeleteTexture(id int64) error
+IncrementTextureDownloadCount(id int64) error
+IncrementTextureFavoriteCount(id int64) error
+DecrementTextureFavoriteCount(id int64) error
+IsTextureFavorited(userID, textureID int64) (bool, error)
}
TextureService --> Texture : "操作"
TextureService --> Repository : "调用"
```
**图示来源**
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [texture.go](file://internal/model/texture.go#L1-L77)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
## 服务调用关系与流程
服务层的调用流程通常始于`handler`层的HTTP请求处理器。例如当用户发起注册请求时`auth_handler.go`中的处理器会调用`UserService.RegisterUser`方法。该方法内部会协调多个Repository操作检查用户、创建用户并依赖`pkg/auth`包进行密码加密和Token生成。
服务之间通常是独立的,但它们都依赖于`Repository`层来访问数据。例如,`ProfileService.CreateProfile`在创建档案时,会先调用`repository.FindUserByID`来验证用户,这与`UserService`使用的Repository是同一个体现了数据访问层的共享性。
一个典型的跨服务流程是用户上传材质并将其设置为档案皮肤:
1. `TextureService.CreateTexture` 被调用,创建材质记录。
2. `ProfileService.UpdateProfile` 被调用传入新创建的材质ID作为`skinID`参数,更新档案的皮肤引用。
这种设计保证了服务的单一职责同时通过Repository层实现了数据的统一管理。
## 业务规则实现
服务层是复杂业务规则的主要实现者。
**档案数量限制**
`ProfileService.CheckProfileLimit`方法通过调用`repository.CountProfilesByUserID`获取用户当前的档案数量,并与传入的`maxProfiles`上限进行比较。这个检查通常在`CreateProfile`方法的开头执行,以防止用户创建过多的档案。
**材质上传流程**
`TextureService.CreateTexture`方法实现了严格的上传流程:
1. **用户验证**:确保上传者是有效的用户。
2. **去重检查**:通过`repository.FindTextureByHash`检查材质的Hash值避免存储重复内容。
3. **类型验证**:使用`switch`语句确保`textureType`是有效的("SKIN"或"CAPE")。
4. **数据创建**:构建`model.Texture`对象并调用`repository.CreateTexture`进行持久化。
**活跃档案管理**
`ProfileService.SetActiveProfile`是实现“一个用户只能有一个活跃档案”这一业务规则的核心。它利用GORM的事务功能在一个原子操作中完成两步
1. 将用户所有档案的`is_active`字段更新为`false`
2. 将指定档案的`is_active`字段更新为`true`
这确保了即使在高并发场景下,也不会出现多个档案同时为活跃状态的情况。
## 错误处理与日志记录
服务层实现了细粒度的错误处理。每个方法都会返回一个`error`类型的值,用于向调用者传递错误信息。错误通常使用`fmt.Errorf`进行包装,以提供上下文信息,例如`fmt.Errorf("查询用户失败: %w", err)`
此外,服务层还直接负责记录关键操作日志:
- `UserService`中的`logSuccessLogin``logFailedLogin`方法会创建`UserLoginLog`记录,用于审计登录行为。
- `TextureService`中的`RecordTextureDownload`方法会创建`TextureDownloadLog`记录,用于追踪下载量。
这些日志记录通过调用相应的Repository方法`repository.CreateLoginLog`)来持久化,为系统监控和问题排查提供了重要依据。
**本节来源**
- [user_service.go](file://internal/service/user_service.go#L203-L226)
- [texture_service.go](file://internal/service/texture_service.go#L162-L187)
- [user_repository.go](file://internal/repository/user_repository.go#L77-L81)
- [texture_repository.go](file://internal/repository/texture_repository.go#L153-L157)
## 结论
CarrotSkin项目的服务层设计清晰、职责分明有效地封装了核心业务逻辑。`UserService``ProfileService``TextureService`三个核心服务各司其职通过调用统一的Repository层来访问数据实现了业务逻辑与数据持久化的解耦。服务层成功实现了档案数量限制、材质上传去重、活跃档案管理等复杂业务规则并通过事务和原子操作保证了数据的一致性。其良好的错误处理和日志记录机制为系统的稳定运行和维护提供了保障。该架构为未来的功能扩展如积分系统、审核流程奠定了坚实的基础。

View File

@@ -1,462 +0,0 @@
# 材质服务
<cite>
**本文引用的文件**
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
- [internal/model/texture.go](file://internal/model/texture.go)
- [pkg/database/manager.go](file://pkg/database/manager.go)
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者系统性梳理材质服务TextureService的职责、方法与内部逻辑重点覆盖
- 材质类型SKIN/CAPE校验与默认值设定状态、下载数、收藏数
- 材质状态管理(正常、禁用、删除)
- 上传、搜索、收藏等业务规则及服务层与仓储层的协作
- 状态验证逻辑(状态为-1表示已删除无效
- 常见问题与错误处理策略
- 性能优化建议(如高效查询热门材质)
## 项目结构
围绕材质服务的关键文件组织如下:
- 服务层:负责业务规则编排与跨仓储调用
- 仓储层:封装数据库访问与聚合查询
- 模型层:定义材质实体、收藏关联与下载日志
- 数据库管理:统一数据库连接与自动迁移
```mermaid
graph TB
subgraph "服务层"
S1["TextureService<br/>internal/service/texture_service.go"]
end
subgraph "仓储层"
R1["TextureRepository<br/>internal/repository/texture_repository.go"]
end
subgraph "模型层"
M1["Texture<br/>internal/model/texture.go"]
M2["UserTextureFavorite<br/>internal/model/texture.go"]
M3["TextureDownloadLog<br/>internal/model/texture.go"]
end
subgraph "基础设施"
D1["数据库管理器<br/>pkg/database/manager.go"]
end
S1 --> R1
R1 --> D1
S1 --> M1
S1 --> M2
S1 --> M3
R1 --> M1
R1 --> M2
R1 --> M3
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
## 核心组件
- TextureService提供材质上传、查询、更新、删除、下载统计、收藏切换、收藏列表、上传数量限制检查等能力
- TextureRepository封装材质的增删改查、分页、统计、收藏与下载日志写入等仓储操作
- Model.Texture材质实体包含类型、状态、公开度、下载/收藏计数、Slim标记等
- Model.UserTextureFavorite用户-材质收藏关联
- Model.TextureDownloadLog材质下载日志
- Database Manager数据库连接与自动迁移
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
## 架构总览
服务层通过统一的 GORM 数据库实例访问仓储层,仓储层对数据库执行具体操作;模型层定义实体与索引约束,确保查询与统计效率。
```mermaid
sequenceDiagram
participant C as "调用方"
participant S as "TextureService"
participant R as "TextureRepository"
participant DB as "GORM DB"
participant M as "Model"
C->>S : 调用上传/搜索/收藏等方法
S->>R : 调用仓储方法查询/更新/写入
R->>DB : 执行数据库操作
DB-->>R : 返回结果/影响行数
R-->>S : 返回实体/列表/统计
S-->>C : 返回业务结果
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
## 详细组件分析
### TextureService 方法与职责
- 创建材质
- 校验上传者存在性
- 基于哈希去重
- 类型转换与校验(仅支持 SKIN/CAPE
- 默认状态为“正常”,默认下载数与收藏数为 0
- 写入仓储并返回实体
- 获取材质详情
- 查询材质并校验状态(状态为-1视为已删除
- 用户上传列表
- 分页查询,排除已删除状态
- 搜索材质
- 支持公开筛选、类型筛选、关键词模糊匹配
- 分页查询,按创建时间倒序
- 更新材质
- 权限校验(仅上传者可更新)
- 动态更新名称/描述/公开状态
- 删除材质
- 权限校验(仅上传者可删除)
- 采用软删除(状态置为-1
- 记录下载
- 校验材质存在性
- 原子递增下载计数
- 写入下载日志
- 切换收藏
- 校验材质存在性
- 已收藏则取消并递减收藏计数;未收藏则添加并递增收藏计数
- 用户收藏列表
- 分页查询收藏的材质(排除已删除)
- 上传数量限制检查
- 统计用户未删除材质数量并与上限比较
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
### 材质类型与默认值
- 类型枚举
- SKIN、CAPE 两种类型
- 默认值
- 状态1正常
- 下载数0
- 收藏数0
- Slim 标记:用于区分 Alex/Steve 角色皮肤样式
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
### 状态管理与验证
- 状态含义
- 1正常
- 0审核中/禁用(仍存在,但不可用)
- -1已删除软删除
- 验证规则
- 获取详情时若状态为-1判定为“已删除”
- 搜索与收藏列表均排除状态为-1的材质
- 删除操作采用软删除,不物理删除
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
### 上传、搜索与收藏业务规则
- 上传
- 哈希唯一性约束,避免重复
- 类型严格校验
- 默认状态与计数初始化
- 搜索
- 公开筛选、类型筛选、关键词模糊匹配
- 分页与排序(按创建时间倒序)
- 收藏
- 基于用户-材质关联表维护收藏
- 切换收藏时原子更新计数
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
### 服务层与仓储层协作
- 服务层负责:
- 参数校验与业务规则
- 权限校验(上传者)
- 组合仓储调用以完成复杂流程(如收藏切换)
- 仓储层负责:
- 数据库 CRUD、分页、统计、索引使用
- 原子计数更新(下载/收藏)
```mermaid
classDiagram
class TextureService {
+CreateTexture(...)
+GetTextureByID(...)
+GetUserTextures(...)
+SearchTextures(...)
+UpdateTexture(...)
+DeleteTexture(...)
+RecordTextureDownload(...)
+ToggleTextureFavorite(...)
+GetUserTextureFavorites(...)
+CheckTextureUploadLimit(...)
}
class TextureRepository {
+CreateTexture(...)
+FindTextureByID(...)
+FindTextureByHash(...)
+FindTexturesByUploaderID(...)
+SearchTextures(...)
+UpdateTexture(...)
+UpdateTextureFields(...)
+DeleteTexture(...)
+IncrementTextureDownloadCount(...)
+IncrementTextureFavoriteCount(...)
+DecrementTextureFavoriteCount(...)
+CreateTextureDownloadLog(...)
+IsTextureFavorited(...)
+AddTextureFavorite(...)
+RemoveTextureFavorite(...)
+GetUserTextureFavorites(...)
+CountTexturesByUploaderID(...)
}
class Texture
class UserTextureFavorite
class TextureDownloadLog
TextureService --> TextureRepository : "调用"
TextureRepository --> Texture : "读写"
TextureRepository --> UserTextureFavorite : "读写"
TextureRepository --> TextureDownloadLog : "读写"
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
### 关键流程图示
#### 获取材质详情流程
```mermaid
flowchart TD
Start(["进入 GetTextureByID"]) --> Load["仓储查询材质"]
Load --> Found{"找到记录?"}
Found --> |否| NotFound["返回不存在错误"]
Found --> |是| CheckStatus["检查状态"]
CheckStatus --> Deleted{"状态为-1"}
Deleted --> |是| DelErr["返回已删除错误"]
Deleted --> |否| Return["返回材质对象"]
NotFound --> End(["结束"])
DelErr --> End
Return --> End
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L16-L27)
#### 切换收藏流程
```mermaid
flowchart TD
Start(["进入 ToggleTextureFavorite"]) --> Load["仓储查询材质"]
Load --> Found{"找到记录?"}
Found --> |否| Err["返回不存在错误"]
Found --> |是| CheckFav["检查是否已收藏"]
CheckFav --> IsFav{"已收藏?"}
IsFav --> |是| Unfav["删除收藏记录"]
Unfav --> Dec["递减收藏计数"]
IsFav --> |否| Fav["新增收藏记录"]
Fav --> Inc["递增收藏计数"]
Dec --> Done["返回false"]
Inc --> Done2["返回true"]
Err --> End(["结束"])
Done --> End
Done2 --> End
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
#### 上传流程(含类型与去重校验)
```mermaid
flowchart TD
Start(["进入 CreateTexture"]) --> CheckUser["校验上传者存在"]
CheckUser --> Exists{"存在?"}
Exists --> |否| UErr["返回用户不存在错误"]
Exists --> |是| CheckHash["按哈希查询材质"]
CheckHash --> Dup{"已存在?"}
Dup --> |是| DupErr["返回材质已存在错误"]
Dup --> |否| ParseType["解析类型(SKIN/CAPE)"]
ParseType --> Valid{"类型有效?"}
Valid --> |否| TErr["返回无效类型错误"]
Valid --> |是| Build["构建材质对象(默认状态/计数)"]
Build --> Save["仓储保存材质"]
Save --> Ok["返回材质对象"]
UErr --> End(["结束"])
DupErr --> End
TErr --> End
Ok --> End
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L29-L41)
## 依赖关系分析
- 服务层依赖仓储层接口,仓储层依赖数据库管理器提供的全局 GORM 实例
- 模型层定义了材质、收藏、下载日志三类实体,并带有索引与排序提示
- 数据库管理器负责初始化与迁移,确保表结构与模型一致
```mermaid
graph LR
TS["TextureService"] --> TR["TextureRepository"]
TR --> DM["Database Manager"]
TR --> M["Models(Texture/UserTextureFavorite/TextureDownloadLog)"]
```
图表来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
## 性能考虑
- 索引与排序
- 材质表对公开状态、类型、状态组合建立索引,有利于搜索过滤
- 下载数与收藏数列带索引且按降序排序,便于热门查询
- 原子计数更新
- 下载与收藏计数采用原生表达式递增/递减,减少锁竞争与往返
- 分页与预加载
- 分页查询时使用偏移与限制,结合预加载提升列表渲染效率
- 热门查询建议
- 使用下载数/收藏数列的降序索引进行 Top-N 查询
- 对搜索场景,优先利用公开状态与类型过滤,缩小扫描范围
- 缓存策略(建议)
- 对热点材质详情与热门榜单可引入缓存层,降低数据库压力
- 结合 TTL 与失效策略,保证数据一致性
章节来源
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L93-L103)
## 故障排查指南
- 无效类型
- 现象:上传时返回“无效的材质类型”
- 排查:确认传入类型为 SKIN 或 CAPE
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L32-L41)
- 材质已存在
- 现象:上传返回“该材质已存在”
- 排查:确认哈希是否重复;检查去重逻辑
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31)
- 用户不存在
- 现象:上传返回“用户不存在”
- 排查确认上传者ID正确检查用户是否存在
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L14-L21)
- 材质不存在或已删除
- 现象:获取详情/收藏/下载时返回“材质不存在”或“材质已删除”
- 排查确认ID正确检查状态是否为-1
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
- 权限不足
- 现象:更新/删除返回“无权修改/无权删除”
- 排查:确认请求者为上传者
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L106-L141)
- 上传数量超限
- 现象:检查上传限制返回已达上限
- 排查:确认当前未删除材质数量与上限配置
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L106-L141)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
## 结论
TextureService 将业务规则与仓储操作解耦,通过严格的类型与状态校验、完善的权限控制与原子计数更新,保障了材质上传、搜索、收藏与统计的可靠性。配合模型层的索引设计与服务层的分页策略,可在高并发场景下保持良好性能。建议在热点场景引入缓存与更精细的索引策略,持续优化查询与写入性能。
## 附录
### API 一览(方法与关键行为)
- CreateTexture
- 输入上传者ID、名称、描述、类型、URL、哈希、大小、公开/Slim、默认状态与计数
- 行为:校验用户与哈希唯一性,类型转换,创建并返回
- 错误:用户不存在、哈希重复、无效类型
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
- GetTextureByID
- 输入材质ID
- 行为:查询并校验状态(-1视为已删除
- 错误:不存在、已删除
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
- GetUserTextures
- 输入上传者ID、分页参数
- 行为:分页查询用户上传材质(排除已删除)
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L81-L91)
- SearchTextures
- 输入:关键词、类型、公开筛选、分页参数
- 行为:按条件过滤并分页,按创建时间倒序
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L93-L103)
- UpdateTexture
- 输入材质ID、上传者ID、名称/描述/公开状态
- 行为:权限校验后动态更新字段
- 错误:不存在、权限不足
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L106-L141)
- DeleteTexture
- 输入材质ID、上传者ID
- 行为:权限校验后软删除(状态=-1
- 错误:不存在、权限不足
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L144-L160)
- RecordTextureDownload
- 输入材质ID、用户ID/IP/UA
- 行为:原子递增下载计数并写入日志
- 错误:不存在
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187)
- ToggleTextureFavorite
- 输入用户ID、材质ID
- 行为:切换收藏并原子更新计数
- 错误:不存在
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
- GetUserTextureFavorites
- 输入用户ID、分页参数
- 行为:分页查询收藏的材质(排除已删除)
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L227-L237)
- CheckTextureUploadLimit
- 输入上传者ID、最大数量
- 行为:统计未删除材质数量并与上限比较
- 错误:达到上限
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
### 状态与类型验证(测试参考)
- 类型验证:仅接受 SKIN/CAPE
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L7-L44)
- 默认值验证:状态=1下载数=0收藏数=0
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L46-L65)
- 状态验证:状态=-1 无效
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L67-L99)
- 分页参数校验page≥11≤pageSize≤100
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L102-L161)
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)

View File

@@ -1,365 +0,0 @@
# 档案服务
<cite>
**本文引用的文件**
- [profile_service.go](file://internal/service/profile_service.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [profile.go](file://internal/model/profile.go)
- [user.go](file://internal/model/user.go)
- [texture.go](file://internal/model/texture.go)
- [profile_handler.go](file://internal/handler/profile_handler.go)
- [common.go](file://internal/types/common.go)
- [profile_service_test.go](file://internal/service/profile_service_test.go)
- [profile_handler_test.go](file://internal/handler/profile_handler_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本档“档案服务”文档聚焦于 ProfileService 的职责、方法与内部逻辑覆盖档案的创建、更新、删除与活跃状态管理明确档案名称与用户ID的验证规则名称非空且长度1-16用户ID大于0以及状态有效性判断仅当状态为1时表示正常可用阐述新创建档案默认为活跃状态的业务规则并说明多档案用户的活跃档案切换逻辑解释档案与用户、材质之间的关联关系最后提供常见错误场景的排查指南帮助开发者快速定位问题。
## 项目结构
档案服务位于 internal/service 层,通过 internal/handler 接收HTTP请求调用 ProfileService 完成业务逻辑,再由 ProfileService 调用 internal/repository 访问数据库;数据模型定义在 internal/model 中,类型定义在 internal/types 中。
```mermaid
graph TB
subgraph "接口层"
H["profile_handler.go<br/>HTTP处理器"]
end
subgraph "服务层"
S["profile_service.go<br/>ProfileService"]
end
subgraph "仓储层"
R["profile_repository.go<br/>Profile仓储"]
end
subgraph "模型层"
M1["profile.go<br/>Profile模型"]
M2["user.go<br/>User模型"]
M3["texture.go<br/>Texture模型"]
end
subgraph "类型定义"
T["common.go<br/>CreateProfileRequest/UpdateProfileRequest/ProfileInfo"]
end
H --> S
S --> R
R --> M1
R --> M2
R --> M3
H --> T
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [profile.go](file://internal/model/profile.go#L1-L64)
- [user.go](file://internal/model/user.go#L1-L71)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [common.go](file://internal/types/common.go#L81-L207)
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [profile.go](file://internal/model/profile.go#L1-L64)
- [user.go](file://internal/model/user.go#L1-L71)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [common.go](file://internal/types/common.go#L81-L207)
## 核心组件
- ProfileService负责档案的创建、查询、更新、删除、活跃状态设置与数量限制检查等核心业务逻辑。
- ProfileRepository封装数据库访问包括创建、查询、更新、删除、统计、设置活跃状态等。
- Profile 模型:定义档案的数据结构及与用户、材质的关联。
- Handler接收HTTP请求解析参数调用服务层并返回统一响应。
- 类型定义CreateProfileRequest、UpdateProfileRequest、ProfileInfo 等用于API契约与响应结构。
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [profile.go](file://internal/model/profile.go#L1-L64)
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [common.go](file://internal/types/common.go#L81-L207)
## 架构总览
档案服务采用典型的三层架构Handler 负责接口与参数校验Service 负责业务规则与流程编排Repository 负责数据持久化。ProfileService 在创建档案时会进行用户存在性与状态校验、角色名唯一性校验、生成UUID与RSA密钥、创建档案并将其设置为活跃状态在设置活跃状态时通过事务将该用户下的其他档案全部置为非活跃确保同一时刻仅有一个活跃档案。
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "Handler"
participant S as "ProfileService"
participant R as "ProfileRepository"
participant DB as "数据库"
C->>H : "POST /api/v1/profile"
H->>H : "解析请求体(CreateProfileRequest)"
H->>S : "CheckProfileLimit(userID, maxProfiles)"
S->>R : "CountProfilesByUserID(userID)"
R-->>S : "count"
S-->>H : "通过/错误"
H->>S : "CreateProfile(userID, name)"
S->>R : "FindUserByID(userID)"
R-->>S : "User"
S->>R : "FindProfileByName(name)"
R-->>S : "Profile 或 NotFound"
S->>S : "生成UUID与RSA私钥"
S->>R : "CreateProfile(Profile)"
R->>DB : "INSERT"
DB-->>R : "OK"
S->>R : "SetActiveProfile(uuid, userID)"
R->>DB : "事务 : 先将用户其他档案置为非活跃,再将目标置为活跃"
DB-->>R : "OK"
R-->>S : "OK"
S-->>H : "Profile"
H-->>C : "200 成功响应"
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
- [profile_service.go](file://internal/service/profile_service.go#L17-L68)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
## 详细组件分析
### ProfileService 方法与职责
- 创建档案
- 输入用户ID、档案名称
- 校验用户存在且状态为1档案名称唯一名称长度1-16
- 生成UUID、RSA私钥PEM格式
- 写入:创建档案记录,默认 IsActive=true
- 并发安全:通过事务将该用户下其他档案置为非活跃,确保仅一个活跃档案
- 获取档案详情
- 输入档案UUID
- 输出Profile预加载Skin与Cape
- 获取用户档案列表
- 输入用户ID
- 输出Profile 列表(按创建时间倒序)
- 更新档案
- 输入UUID、用户ID、可选名称、可选SkinID、可选CapeID
- 校验:权限(档案属于当前用户)、名称唯一性(若更改)
- 更新:保存变更并重新加载以返回最新数据
- 删除档案
- 输入UUID、用户ID
- 校验:权限
- 删除:物理删除档案
- 设置活跃档案
- 输入UUID、用户ID
- 校验:权限
- 事务:将该用户下其他档案置为非活跃,再将目标置为活跃
- 同步:更新最后使用时间
- 档案数量限制
- 输入用户ID、最大数量
- 校验:统计当前档案数是否达到上限
- 校验档案归属
- 输入用户ID、档案UUID
- 校验UUID存在且属于该用户
- 名称批量查询
- 输入:名称数组
- 输出Profile 列表
- 密钥对查询
- 输入档案ID
- 输出KeyPair私钥、公钥、过期时间等
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L17-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L177)
- [profile.go](file://internal/model/profile.go#L1-L64)
### 数据模型与关联关系
- Profile
- 字段UUID、UserID、Name、SkinID、CapeID、RSAPrivateKey、IsActive、LastUsedAt、CreatedAt、UpdatedAt
- 关联User外键 UserID、TextureSkinID、CapeID
- User
- 字段ID、Username、Email、Role、Status1: 正常, 0: 禁用, -1: 删除)
- Texture
- 字段ID、UploaderID、Name、Type、URL、Hash、IsPublic、DownloadCount、FavoriteCount、IsSlim、Status1: 正常, 0: 审核中, -1: 已删除)
```mermaid
erDiagram
USER {
bigint id PK
varchar username UK
varchar email UK
smallint status
}
PROFILE {
varchar uuid PK
bigint user_id FK
varchar name UK
bigint skin_id
bigint cape_id
boolean is_active
timestamp last_used_at
timestamp created_at
timestamp updated_at
}
TEXTURE {
bigint id PK
bigint uploader_id FK
varchar name
varchar type
varchar url
varchar hash UK
boolean is_public
integer download_count
integer favorite_count
boolean is_slim
smallint status
}
USER ||--o{ PROFILE : "拥有"
TEXTURE ||--o{ PROFILE : "被使用(Skin/Cape)"
```
图表来源
- [profile.go](file://internal/model/profile.go#L1-L64)
- [user.go](file://internal/model/user.go#L1-L71)
- [texture.go](file://internal/model/texture.go#L1-L77)
### 验证规则与状态判断
- 档案名称
- 非空且长度在1-16之间
- 更新时仅当名称发生变更才检查唯一性
- 用户ID
- 必须大于0
- 用户状态
- 仅当用户状态为1正常时允许创建档案
- 活跃状态
- 默认 IsActive=true
- 仅当状态为1时表示“正常可用”
- 设置活跃状态时,通过事务将该用户下其他档案置为非活跃
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L17-L68)
- [profile_service_test.go](file://internal/service/profile_service_test.go#L1-L77)
- [common.go](file://internal/types/common.go#L81-L207)
- [user.go](file://internal/model/user.go#L1-L71)
### 活跃档案切换逻辑
- 设计目标:同一用户在同一时刻仅有一个活跃档案
- 实现方式:在设置活跃档案时,使用数据库事务
- 先将该用户下所有档案的 IsActive=false
- 再将目标档案 IsActive=true
- 最后更新最后使用时间
```mermaid
flowchart TD
Start(["开始"]) --> Load["加载档案(uuid, userID)"]
Load --> CheckPerm{"权限校验通过?"}
CheckPerm --> |否| ErrPerm["返回无权操作"]
CheckPerm --> |是| Txn["开启事务"]
Txn --> SetAllFalse["将用户其他档案置为非活跃"]
SetAllFalse --> SetTargetTrue["将目标档案置为活跃"]
SetTargetTrue --> UpdateTime["更新最后使用时间"]
UpdateTime --> Commit["提交事务"]
Commit --> Done(["结束"])
ErrPerm --> Done
```
图表来源
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
### API 与错误处理
- 创建档案
- 请求体CreateProfileRequest仅需 name长度1-16
- 成功:返回 ProfileInfo含 UUID、UserID、Name、IsActive、时间戳等
- 常见错误:未授权、参数错误、已达上限、服务器错误
- 获取档案列表/详情
- 成功:返回 ProfileInfo 列表/对象
- 常见错误:未授权、服务器错误、档案不存在
- 更新档案
- 请求体UpdateProfileRequest可选 name、skin_id、cape_id
- 成功:返回更新后的 ProfileInfo
- 常见错误:未授权、参数错误、无权操作、档案不存在、服务器错误
- 删除档案
- 成功:返回“删除成功”
- 常见错误:未授权、无权操作、档案不存在、服务器错误
- 设置活跃档案
- 成功:返回“设置成功”
- 常见错误:未授权、无权操作、档案不存在、服务器错误
章节来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L399)
- [common.go](file://internal/types/common.go#L81-L207)
## 依赖关系分析
- Handler 依赖 Service
- Service 依赖 Repository
- Repository 依赖 Model 与数据库连接
- Model 间通过外键建立关联
```mermaid
graph LR
H["profile_handler.go"] --> S["profile_service.go"]
S --> R["profile_repository.go"]
R --> M1["profile.go"]
R --> M2["user.go"]
R --> M3["texture.go"]
```
图表来源
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [profile.go](file://internal/model/profile.go#L1-L64)
- [user.go](file://internal/model/user.go#L1-L71)
- [texture.go](file://internal/model/texture.go#L1-L77)
## 性能考量
- 查询预加载:获取档案详情与列表时预加载 Skin 与 Cape减少 N+1 查询风险
- 事务一致性:设置活跃档案使用事务,保证原子性与一致性
- 唯一索引Name 与 Hash 等字段具备唯一索引,降低重复写入成本
- 时间戳LastUsedAt 便于后续统计与清理策略
[本节为通用建议,不涉及具体文件分析]
## 故障排查指南
- 创建失败
- 用户不存在或状态异常检查用户是否存在且状态为1
- 角色名重复:确认名称唯一性
- 达到档案数量上限:检查当前用户档案数量与上限配置
- 数据库错误:查看事务提交与唯一约束冲突
- 更新失败
- 无权操作确认请求用户ID与档案所属用户一致
- 名称重复:若修改了名称,需确保唯一性
- 删除失败
- 无权操作确认请求用户ID与档案所属用户一致
- 设置活跃失败
- 无权操作确认请求用户ID与档案所属用户一致
- 事务回滚:检查数据库事务日志
- 获取失败
- 档案不存在确认UUID正确且未被删除
章节来源
- [profile_service.go](file://internal/service/profile_service.go#L17-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L177)
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L399)
## 结论
ProfileService 提供了完整的档案生命周期管理能力,涵盖创建、查询、更新、删除与活跃状态切换,并通过严格的验证规则与事务保障确保数据一致性。档案与用户、材质的关联清晰,便于扩展更多功能。建议在生产环境中结合日志与监控,持续优化性能与稳定性。
[本节为总结性内容,不涉及具体文件分析]
## 附录
- 关键方法路径参考
- 创建档案:[CreateProfile](file://internal/service/profile_service.go#L17-L68)
- 获取档案详情:[GetProfileByUUID](file://internal/service/profile_service.go#L71-L81)
- 获取用户档案列表:[GetUserProfiles](file://internal/service/profile_service.go#L83-L90)
- 更新档案:[UpdateProfile](file://internal/service/profile_service.go#L92-L135)
- 删除档案:[DeleteProfile](file://internal/service/profile_service.go#L137-L159)
- 设置活跃档案:[SetActiveProfile](file://internal/service/profile_service.go#L161-L188)
- 数量限制检查:[CheckProfileLimit](file://internal/service/profile_service.go#L190-L202)
- 校验档案归属:[ValidateProfileByUserID](file://internal/service/profile_service.go#L222-L235)
- 名称批量查询:[GetProfilesDataByNames](file://internal/service/profile_service.go#L237-L243)
- 密钥对查询:[GetProfileKeyPair](file://internal/service/profile_service.go#L245-L253)
- Handler 路由与错误映射
- 创建/获取/更新/删除/设置活跃:[profile_handler.go](file://internal/handler/profile_handler.go#L28-L399)

View File

@@ -1,372 +0,0 @@
# 用户服务
<cite>
**本文引用的文件列表**
- [internal/service/user_service.go](file://internal/service/user_service.go)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
- [internal/model/user.go](file://internal/model/user.go)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
- [pkg/auth/password.go](file://pkg/auth/password.go)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go)
- [internal/types/common.go](file://internal/types/common.go)
- [internal/service/common.go](file://internal/service/common.go)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
- [internal/model/system_config.go](file://internal/model/system_config.go)
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件聚焦于用户服务UserService的职责、方法与内部逻辑覆盖用户注册、登录、信息更新等核心功能并对默认角色、状态与积分的初始化进行说明同时解释头像选择策略优先自定义头像否则使用默认头像以及数据验证规则用户名、邮箱、密码非空且邮箱格式正确。文档还提供服务层与仓储层的调用关系示例帮助初学者理解业务流程并为有经验的开发者提供扩展点建议如修改默认角色、添加新的用户属性
## 项目结构
用户服务位于 internal/service 层,围绕 internal/model 定义的数据模型工作,通过 internal/repository 与数据库交互,对外由 internal/handler 提供 HTTP 接口。认证相关的密码处理与 JWT 令牌生成分别由 pkg/auth 下的 password 与 jwt 组件完成。
```mermaid
graph TB
subgraph "接口层"
H["auth_handler<br/>HTTP接口"]
end
subgraph "服务层"
S["user_service<br/>用户业务逻辑"]
end
subgraph "模型层"
M["user.go<br/>User/UserLoginLog/UserPointLog"]
SCM["system_config.go<br/>SystemConfig"]
end
subgraph "仓储层"
R["user_repository<br/>CRUD/事务"]
SCR["system_config_repository<br/>系统配置查询"]
end
subgraph "认证与工具"
P["password.go<br/>密码加解密"]
J["jwt.go<br/>JWT签发/校验"]
T["types/common.go<br/>请求/响应结构"]
end
H --> S
S --> R
S --> SCR
S --> P
S --> J
S --> M
S --> SCM
T --> H
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
## 核心组件
- 用户模型User包含用户名、邮箱、头像、积分、角色、状态、最后登录时间等字段以及唯一索引约束与默认值。
- 用户登录日志UserLoginLog记录登录尝试的IP、UA、是否成功及失败原因。
- 用户积分日志UserPointLog记录积分变动明细类型、金额、前后余额、原因等
- 系统配置SystemConfig键值型配置用于存储默认头像等全局设置。
- 服务层UserService封装注册、登录、信息更新、头像更新、密码修改/重置、邮箱变更等业务逻辑。
- 仓储层UserRepository提供用户与登录/积分日志的数据库操作,包含事务性积分更新。
- 认证工具Password/JWT密码加密与校验、JWT签发与校验。
- 接口层AuthHandler接收HTTP请求绑定参数调用服务层并返回统一响应。
章节来源
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
## 架构总览
下图展示从HTTP请求到服务层再到仓储层的整体调用链路以及关键对象之间的关系。
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "AuthHandler"
participant S as "UserService"
participant P as "Password"
participant J as "JWTService"
participant R as "UserRepository"
participant SCR as "SystemConfigRepository"
C->>H : "POST /api/v1/auth/register"
H->>H : "绑定RegisterRequest并校验"
H->>S : "RegisterUser(jwtService, username, password, email, avatar)"
S->>R : "FindUserByUsername(username)"
S->>R : "FindUserByEmail(email)"
S->>P : "HashPassword(password)"
S->>SCR : "GetSystemConfigByKey('default_avatar')"
S->>R : "CreateUser(user)"
S->>J : "GenerateToken(user.id, user.username, user.role)"
S-->>H : "返回user, token"
H-->>C : "返回LoginResponse"
C->>H : "POST /api/v1/auth/login"
H->>S : "LoginUser(jwtService, usernameOrEmail, password, ip, ua)"
S->>R : "按用户名或邮箱查找用户"
S->>S : "检查用户状态"
S->>P : "CheckPassword(user.password, password)"
S->>R : "UpdateUserFields(last_login_at)"
S->>J : "GenerateToken(...)"
S-->>H : "返回user, token"
H-->>C : "返回LoginResponse"
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
## 详细组件分析
### 用户服务UserService职责与方法
- 注册RegisterUser
- 校验用户名与邮箱唯一性
- 密码加密
- 头像选择:若传入自定义头像则使用,否则从系统配置读取默认头像
- 初始化角色为“user”状态为1正常积分初始为0
- 创建用户并生成JWT
- 登录LoginUser
- 支持用户名或邮箱登录(通过是否包含“@”判断)
- 校验用户状态仅允许状态为1的用户登录
- 验证密码
- 生成JWT并更新最后登录时间
- 记录登录日志(成功/失败)
- 查询与更新
- 按ID获取用户
- 更新用户信息(完整保存)
- 更新头像(字段更新)
- 修改密码(校验旧密码后加密新密码并更新)
- 重置密码(通过邮箱查找用户并更新密码)
- 更换邮箱(校验新邮箱唯一性后更新)
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
### 数据模型与默认值
- 用户模型字段与默认值
- 角色默认“user”
- 状态默认1正常
- 积分默认0
- 头像:默认空字符串
- 时间戳createdAt/updatedAt默认当前时间
- 登录日志与积分日志
- 登录日志包含IP、UA、登录方式、是否成功、失败原因
- 积分日志包含变更类型、金额、前后余额、原因等
章节来源
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
### 头像逻辑(默认头像策略)
- 优先使用用户提供的头像URL
- 若未提供则从系统配置中读取键为“default_avatar”的值作为默认头像
- 若系统配置不存在,则返回提示信息字符串
```mermaid
flowchart TD
Start(["开始"]) --> CheckProvided["是否提供了头像URL"]
CheckProvided --> |是| UseProvided["使用提供的头像URL"]
CheckProvided --> |否| LoadConfig["从系统配置读取default_avatar"]
LoadConfig --> Found{"配置是否存在?"}
Found --> |是| UseDefault["使用系统配置中的默认头像URL"]
Found --> |否| UseError["返回默认头像配置缺失提示"]
UseProvided --> End(["结束"])
UseDefault --> End
UseError --> End
```
图表来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L228-L240)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L23)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L228-L240)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L23)
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
### 数据验证规则
- 请求体绑定规则(由接口层负责)
- 注册用户名必填且长度在3-50之间邮箱必填且符合邮箱格式密码必填且长度6-128验证码必填且长度6头像可选且需为合法URL
- 登录用户名必填支持用户名或邮箱密码必填且长度6-128
- 更新用户头像可选且为合法URL修改密码时旧密码与新密码长度要求
- 服务层内部验证
- 注册时对用户名、邮箱、密码进行基本非空与邮箱格式校验
- 登录时对状态进行检查,密码进行校验
- 更换邮箱时确保新邮箱未被他人使用
章节来源
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go#L1-L200)
### 服务层与仓储层调用关系示例
- 注册流程
- Handler -> UserService.RegisterUser -> UserRepository.FindUserByUsername/ByEmail -> Password.HashPassword -> SystemConfigRepository.GetSystemConfigByKey -> UserRepository.CreateUser -> JWTService.GenerateToken
- 登录流程
- Handler -> UserService.LoginUser -> UserRepository.FindUserByUsername/ByEmail -> Password.CheckPassword -> UserRepository.UpdateUserFields -> JWTService.GenerateToken
```mermaid
classDiagram
class AuthHandler {
+Register()
+Login()
}
class UserService {
+RegisterUser(...)
+LoginUser(...)
+GetUserByID(...)
+UpdateUserInfo(...)
+UpdateUserAvatar(...)
+ChangeUserPassword(...)
+ResetUserPassword(...)
+ChangeUserEmail(...)
}
class UserRepository {
+CreateUser(...)
+FindUserByID(...)
+FindUserByUsername(...)
+FindUserByEmail(...)
+UpdateUser(...)
+UpdateUserFields(...)
+CreateLoginLog(...)
+UpdateUserPoints(...)
}
class SystemConfigRepository {
+GetSystemConfigByKey(...)
}
class Password {
+HashPassword(...)
+CheckPassword(...)
}
class JWTService {
+GenerateToken(...)
+ValidateToken(...)
}
AuthHandler --> UserService : "调用"
UserService --> UserRepository : "CRUD/事务"
UserService --> SystemConfigRepository : "读取默认头像"
UserService --> Password : "密码处理"
UserService --> JWTService : "签发Token"
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
## 依赖分析
- 内部依赖
- UserService 依赖 UserRepository、SystemConfigRepository、Password、JWTService、User模型、SystemConfig模型
- AuthHandler 依赖 UserService、Types请求/响应、Logger、Redis验证码、Email发送验证码
- 外部依赖
- bcrypt 用于密码加密与校验
- golang-jwt/jwt/v5 用于JWT签发与校验
- GORM 用于数据库访问与事务
```mermaid
graph LR
H["AuthHandler"] --> S["UserService"]
S --> R["UserRepository"]
S --> SCR["SystemConfigRepository"]
S --> P["Password(bcrypt)"]
S --> J["JWTService(gojwt)"]
S --> M["User/SystemConfig Models"]
R --> DB["GORM/DB"]
SCR --> DB
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
## 性能考虑
- 密码加密成本bcrypt 默认成本较高,建议在高并发场景下关注注册/登录的延迟,必要时评估成本参数与异步化策略
- 数据库索引:用户名与邮箱字段具备唯一索引,有助于快速去重;登录日志与积分日志按时间建立索引,有利于查询与统计
- 事务一致性:积分变更采用事务,避免并发写入导致的余额不一致
- 缓存策略:默认头像配置可考虑缓存以减少数据库查询次数
## 故障排查指南
- 注册失败
- 检查用户名/邮箱是否重复
- 确认密码加密是否成功
- 检查系统配置中是否存在“default_avatar”
- 登录失败
- 用户不存在或状态非1
- 密码校验失败
- 检查IP与User-Agent是否正确传入
- 头像显示异常
- 自定义头像URL是否有效
- 系统默认头像配置是否正确
- 集成测试参考
- 单元测试覆盖了默认头像逻辑、邮箱检测、常量与验证逻辑,可据此定位问题
章节来源
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go#L1-L200)
## 结论
UserService 以清晰的职责划分实现了用户生命周期管理:从注册、登录到信息维护与安全控制。通过明确的默认值与验证规则,结合仓储层的事务与模型层的结构化设计,系统在保证安全性的同时具备良好的可扩展性。对于扩展需求,可在服务层增加新的默认值或属性映射,并在仓储层补充相应的查询/更新逻辑。
## 附录
### 扩展点指导
- 修改默认角色
- 在注册流程中调整角色初始化值
- 若需动态分配角色,可引入系统配置或权限策略模块
- 添加新的用户属性
- 在 User 模型中新增字段并设置默认值
- 在注册流程中初始化该字段
- 在接口层的请求/响应结构中同步新增字段
- 默认积分初始化
- 当前初始积分为0可通过系统配置读取或硬编码调整
- 注册奖励积分可作为后续增强点服务层已有TODO标记
章节来源
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
- [internal/service/common.go](file://internal/service/common.go#L1-L14)

View File

@@ -1,310 +0,0 @@
# JWT认证
<cite>
**本文档引用的文件**
- [jwt.go](file://pkg/auth/jwt.go)
- [manager.go](file://pkg/auth/manager.go)
- [auth.go](file://internal/middleware/auth.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [user_service.go](file://internal/service/user_service.go)
- [config.go](file://pkg/config/config.go)
</cite>
## 目录
1. [简介](#简介)
2. [JWT服务结构设计](#jwt服务结构设计)
3. [令牌生成与验证机制](#令牌生成与验证机制)
4. [认证中间件实现](#认证中间件实现)
5. [JWT工作流程](#jwt工作流程)
6. [密钥管理与安全策略](#密钥管理与安全策略)
7. [单例模式应用](#单例模式应用)
8. [最佳实践](#最佳实践)
## 简介
CarrotSkin项目采用基于JWTJSON Web Token的用户认证机制为系统提供安全、无状态的用户身份验证。该机制通过在客户端和服务器之间传递加密的令牌来验证用户身份避免了传统会话存储的服务器状态管理开销。本文档详细阐述了JWT服务的实现原理、核心组件设计以及在Gin框架中的集成方式为开发者提供全面的技术参考。
## JWT服务结构设计
JWT服务的核心是`JWTService`结构体,它封装了令牌生成和验证所需的所有配置和方法。该服务通过`Claims`结构体定义了令牌中包含的用户声明信息。
```mermaid
classDiagram
class JWTService {
-secretKey string
-expireHours int
+GenerateToken(userID int64, username string, role string) (string, error)
+ValidateToken(tokenString string) (*Claims, error)
}
class Claims {
+UserID int64
+Username string
+Role string
+RegisteredClaims jwt.RegisteredClaims
}
JWTService --> Claims : "包含"
```
**Diagram sources**
- [jwt.go](file://pkg/auth/jwt.go#L11-L14)
- [jwt.go](file://pkg/auth/jwt.go#L25-L30)
**Section sources**
- [jwt.go](file://pkg/auth/jwt.go#L11-L71)
## 令牌生成与验证机制
### 令牌生成GenerateToken
`GenerateToken`方法负责创建JWT令牌它接收用户ID、用户名和角色作为参数构建包含这些信息的声明Claims并使用预设的密钥进行签名。
```mermaid
flowchart TD
Start([开始生成令牌]) --> CreateClaims["创建声明对象<br/>包含user_id、username、role"]
CreateClaims --> SetExpiry["设置过期时间<br/>(当前时间 + expireHours)"]
SetExpiry --> SetIssued["设置签发时间<br/>(当前时间)"]
SetIssued --> SetNotBefore["设置生效时间<br/>(当前时间)"]
SetNotBefore --> SetIssuer["设置发行者<br/>(carrotskin)"]
SetIssuer --> CreateToken["创建JWT令牌<br/>使用HS256算法"]
CreateToken --> SignToken["使用secretKey签名"]
SignToken --> ReturnToken["返回签名后的令牌字符串"]
ReturnToken --> End([结束])
```
**Diagram sources**
- [jwt.go](file://pkg/auth/jwt.go#L33-L53)
### 令牌验证ValidateToken
`ValidateToken`方法负责验证接收到的JWT令牌的有效性包括签名验证、过期检查和声明解析。
```mermaid
flowchart TD
Start([开始验证令牌]) --> ParseToken["解析令牌字符串"]
ParseToken --> CheckSignature["验证签名<br/>使用secretKey"]
CheckSignature --> IsValid{"签名有效?"}
IsValid --> |否| ReturnError["返回错误<br/>无效的token"]
IsValid --> |是| CheckClaims["检查声明对象"]
CheckClaims --> IsClaimsValid{"声明类型正确<br/>且令牌有效?"}
IsClaimsValid --> |否| ReturnError
IsClaimsValid --> |是| ReturnClaims["返回解析出的Claims对象"]
ReturnClaims --> End([结束])
ReturnError --> End
```
**Diagram sources**
- [jwt.go](file://pkg/auth/jwt.go#L56-L70)
**Section sources**
- [jwt.go](file://pkg/auth/jwt.go#L32-L70)
## 认证中间件实现
CarrotSkin项目提供了两种认证中间件强制认证中间件AuthMiddleware和可选认证中间件OptionalAuthMiddleware它们在Gin框架中拦截HTTP请求并处理Authorization头。
### 强制认证中间件AuthMiddleware
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Middleware as "AuthMiddleware"
participant JWTService as "JWTService"
Client->>Middleware : 发送请求<br/>Authorization : Bearer token
Middleware->>Middleware : 获取Authorization头
alt 头部为空
Middleware-->>Client : 返回401<br/>缺少Authorization头
return
end
Middleware->>Middleware : 解析Bearer格式
alt 格式无效
Middleware-->>Client : 返回401<br/>无效的Authorization头格式
return
end
Middleware->>JWTService : 调用ValidateToken(token)
alt 验证失败
JWTService-->>Middleware : 返回错误
Middleware-->>Client : 返回401<br/>无效的token
return
end
JWTService-->>Middleware : 返回Claims对象
Middleware->>Middleware : 将用户信息存入上下文<br/>(user_id, username, role)
Middleware->>Client : 继续处理请求
```
### 可选认证中间件OptionalAuthMiddleware
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Middleware as "OptionalAuthMiddleware"
participant JWTService as "JWTService"
Client->>Middleware : 发送请求<br/>Authorization : Bearer token
Middleware->>Middleware : 获取Authorization头
alt 头部存在
Middleware->>Middleware : 解析Bearer格式
alt 格式正确
Middleware->>JWTService : 调用ValidateToken(token)
alt 验证成功
JWTService-->>Middleware : 返回Claims对象
Middleware->>Middleware : 将用户信息存入上下文
end
end
end
Middleware->>Client : 继续处理请求<br/>(无论认证成功与否)
```
**Diagram sources**
- [auth.go](file://internal/middleware/auth.go#L13-L55)
- [auth.go](file://internal/middleware/auth.go#L58-L78)
**Section sources**
- [auth.go](file://internal/middleware/auth.go#L1-L79)
## JWT工作流程
以下是用户从登录到访问受保护资源的完整JWT工作流程
```mermaid
sequenceDiagram
participant User as "用户"
participant Frontend as "前端"
participant Backend as "后端"
User->>Frontend : 输入用户名/邮箱和密码
Frontend->>Backend : POST /api/v1/auth/login
Backend->>Backend : 调用LoginUser服务
Backend->>Backend : 验证用户凭证
alt 验证成功
Backend->>Backend : 调用JWTService.GenerateToken
Backend->>Backend : 生成JWT令牌
Backend-->>Frontend : 返回200<br/>{token, userInfo}
Frontend->>Frontend : 存储令牌
User->>Frontend : 访问受保护页面
Frontend->>Backend : GET /api/v1/protected<br/>Authorization : Bearer token
Backend->>Backend : AuthMiddleware拦截请求
Backend->>Backend : 解析Authorization头
Backend->>Backend : 调用JWTService.ValidateToken
Backend->>Backend : 验证令牌有效性
Backend->>Backend : 提取用户信息存入上下文
Backend->>Backend : 继续处理业务逻辑
Backend-->>Frontend : 返回请求数据
else 验证失败
Backend-->>Frontend : 返回401<br/>登录失败
end
```
**Diagram sources**
- [auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [user_service.go](file://internal/service/user_service.go#L71-L122)
- [auth.go](file://internal/middleware/auth.go#L13-L55)
**Section sources**
- [auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [user_service.go](file://internal/service/user_service.go#L71-L122)
## 密钥管理与安全策略
### 配置管理
JWT服务的配置通过`config.go`文件中的`JWTConfig`结构体进行管理,支持环境变量注入,确保密钥的安全性。
```mermaid
classDiagram
class JWTConfig {
+Secret string
+ExpireHours int
}
class Config {
+Server ServerConfig
+Database DatabaseConfig
+Redis RedisConfig
+JWT JWTConfig
+Casbin CasbinConfig
+Log LogConfig
+Upload UploadConfig
+Email EmailConfig
}
Config --> JWTConfig : "包含"
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L68-L71)
- [config.go](file://pkg/config/config.go#L14-L24)
### 安全声明
JWT令牌中包含的声明Claims具有重要的安全意义
- **user_id**: 用户唯一标识,用于在系统中识别用户身份
- **username**: 用户名,用于显示和审计
- **role**: 用户角色用于基于角色的访问控制RBAC
- **ExpiresAt**: 过期时间,防止令牌长期有效
- **IssuedAt**: 签发时间,用于审计和令牌生命周期管理
- **NotBefore**: 生效时间,可用于延迟生效的令牌
- **Issuer**: 发行者,确保令牌来自可信源
**Section sources**
- [jwt.go](file://pkg/auth/jwt.go#L25-L30)
- [config.go](file://pkg/config/config.go#L68-L71)
## 单例模式应用
`MustGetJWTService`函数实现了线程安全的单例模式确保在整个应用程序生命周期中只存在一个JWT服务实例。
```mermaid
sequenceDiagram
participant Init as "初始化"
participant Get as "GetJWTService"
participant MustGet as "MustGetJWTService"
participant Once as "sync.Once"
Init->>Once : 调用Init(cfg)
Once->>Once : 检查是否已执行
alt 首次调用
Once->>Init : 执行初始化
Init->>jwtServiceInstance : 创建JWTService实例
Init-->>Once : 标记已执行
else 非首次调用
Once-->>Init : 跳过执行
end
Get->>Get : 检查jwtServiceInstance
alt 实例存在
Get-->>Get : 返回实例
else 实例不存在
Get-->>Get : 返回错误
end
MustGet->>Get : 调用GetJWTService
alt 获取成功
Get-->>MustGet : 返回实例
MustGet-->>MustGet : 返回实例
else 获取失败
Get-->>MustGet : 返回错误
MustGet->>MustGet : panic(错误)
end
```
**Diagram sources**
- [manager.go](file://pkg/auth/manager.go#L9-L45)
**Section sources**
- [manager.go](file://pkg/auth/manager.go#L9-L45)
## 最佳实践
### 密钥管理
- JWT密钥`secretKey`)应通过环境变量配置,避免硬编码在代码中
- 使用足够长度的随机字符串作为密钥,提高安全性
- 定期轮换密钥(虽然当前实现不支持,但架构上可以扩展)
### 过期策略
- 当前默认过期时间为168小时7天可通过`JWT_EXPIRE_HOURS`环境变量调整
- 对于敏感操作,建议使用短期令牌或结合刷新令牌机制
- 过期时间不宜过长,以降低令牌泄露后的风险
### 防止重放攻击
- 虽然JWT本身是无状态的但可以通过以下方式增强安全性
- 使用短期令牌
- 在Redis中维护已注销令牌的黑名单
- 结合客户端指纹如IP地址、User-Agent进行额外验证
- 当前实现中,`OptionalAuthMiddleware`提供了基础的灵活性,允许部分接口在认证失败时仍能访问
### 性能考虑
- JWT验证是计算密集型操作但在现代硬件上性能良好
- 使用HS256算法在安全性和性能之间取得了良好平衡
- 避免在令牌中存储过多信息,以减小令牌大小和传输开销
**Section sources**
- [config.go](file://pkg/config/config.go#L164)
- [manager.go](file://pkg/auth/manager.go#L18-L23)
- [jwt.go](file://pkg/auth/jwt.go#L39-L43)

View File

@@ -1,211 +0,0 @@
# RBAC权限控制
<cite>
**本文档引用的文件**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
- [auth.go](file://internal/middleware/auth.go)
- [user_service.go](file://internal/service/user_service.go)
- [user_repository.go](file://internal/repository/user_repository.go)
- [audit_log.go](file://internal/model/audit_log.go)
- [config.go](file://pkg/config/config.go)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql)
</cite>
## 目录
1. [项目结构](#项目结构)
2. [核心组件](#核心组件)
3. [架构概述](#架构概述)
4. [详细组件分析](#详细组件分析)
5. [依赖分析](#依赖分析)
6. [性能考虑](#性能考虑)
7. [故障排除指南](#故障排除指南)
8. [结论](#结论)
## 项目结构
项目采用分层架构设计,权限控制相关文件主要分布在`configs/casbin/``internal/middleware/`目录下。`rbac_model.conf`文件定义了基于Casbin的RBAC权限模型`auth.go`文件实现了JWT认证中间件为权限系统提供用户身份验证。
```mermaid
graph TB
subgraph "配置"
rbac_model_conf[rbac_model.conf]
end
subgraph "中间件"
auth_go[auth.go]
end
subgraph "服务层"
user_service_go[user_service.go]
end
subgraph "数据模型"
audit_log_go[audit_log.go]
end
rbac_model_conf --> auth_go
auth_go --> user_service_go
user_service_go --> audit_log_go
```
**图示来源**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
- [auth.go](file://internal/middleware/auth.go)
- [user_service.go](file://internal/service/user_service.go)
- [audit_log.go](file://internal/model/audit_log.go)
**本节来源**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
- [auth.go](file://internal/middleware/auth.go)
## 核心组件
系统的核心权限控制组件包括Casbin权限模型配置、JWT认证中间件和用户服务。`rbac_model.conf`文件定义了请求定义、策略定义、角色定义、匹配器和策略效果等关键部分。`auth.go`文件中的`AuthMiddleware`函数实现了JWT认证逻辑确保只有经过身份验证的用户才能访问受保护的资源。`user_service.go`文件中的用户服务函数处理用户注册、登录等操作,并在注册时为新用户分配默认角色。
**本节来源**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
- [auth.go](file://internal/middleware/auth.go)
- [user_service.go](file://internal/service/user_service.go)
## 架构概述
系统采用基于Casbin的RBAC基于角色的访问控制权限模型。该模型通过`rbac_model.conf`文件定义包含请求定义、策略定义、角色定义、匹配器和策略效果五个部分。JWT认证中间件负责用户身份验证将用户信息存储在请求上下文中。用户服务处理用户相关的业务逻辑并在注册时为用户分配角色。权限检查在中间件或服务层进行通过将用户角色role与访问资源obj和操作act进行匹配来决定是否允许访问。
```mermaid
graph TD
A[客户端] --> B[JWT认证中间件]
B --> C{身份验证}
C --> |成功| D[权限检查]
C --> |失败| E[返回401]
D --> F{权限允许?}
F --> |是| G[执行操作]
F --> |否| H[返回403]
G --> I[返回结果]
```
**图示来源**
- [auth.go](file://internal/middleware/auth.go)
## 详细组件分析
### RBAC模型配置分析
`rbac_model.conf`文件定义了系统的权限模型。请求定义`r = sub, obj, act`表示权限检查请求包含主体(用户或角色)、对象(资源)和动作(操作)三个部分。策略定义`p = sub, obj, act`定义了权限策略的结构。角色定义`g = _, _`用于建立角色继承关系。匹配器`m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act`是权限检查的核心逻辑,它首先通过`g(r.sub, p.sub)`检查请求主体是否具有策略主体的角色,然后检查请求的对象和动作是否与策略匹配。策略效果`e = some(where (p.eft == allow))`表示只要存在一条允许的策略,就允许访问。
```mermaid
classDiagram
class RBACModel {
+request_definition : r = sub, obj, act
+policy_definition : p = sub, obj, act
+role_definition : g = _, _
+matchers : m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
+policy_effect : e = some(where (p.eft == allow))
}
```
**图示来源**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
**本节来源**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
### JWT认证中间件分析
`auth.go`文件中的`AuthMiddleware`函数实现了JWT认证中间件。该中间件从请求头中提取JWT令牌验证其有效性并将用户信息存储在请求上下文中。`OptionalAuthMiddleware`函数提供了可选的JWT认证允许未认证用户访问某些资源。这两个中间件为系统的权限控制提供了基础的身份验证功能。
```mermaid
sequenceDiagram
participant Client as 客户端
participant Middleware as 认证中间件
participant JWTService as JWT服务
Client->>Middleware : 发送请求(Authorization头)
Middleware->>Middleware : 检查Authorization头
alt 头部不存在
Middleware-->>Client : 返回401(缺少Authorization头)
else 有效格式
Middleware->>JWTService : 验证令牌
alt 令牌有效
JWTService-->>Middleware : 返回用户声明
Middleware->>Middleware : 将用户信息存入上下文
Middleware->>Client : 继续处理请求
else 令牌无效
Middleware-->>Client : 返回401(无效的token)
end
end
```
**图示来源**
- [auth.go](file://internal/middleware/auth.go)
**本节来源**
- [auth.go](file://internal/middleware/auth.go)
### 用户服务分析
`user_service.go`文件中的`RegisterUser`函数处理用户注册逻辑。在创建用户时,该函数将用户的`Role`字段设置为"user",为新用户分配默认角色。`LoginUser`函数处理用户登录,验证用户名/邮箱和密码的正确性并在成功登录后生成JWT令牌。这些服务函数与权限系统紧密集成确保用户在注册和登录时正确地获得和验证其角色。
```mermaid
flowchart TD
Start([注册用户]) --> CheckUsername["检查用户名是否已存在"]
CheckUsername --> UsernameExists{"用户名已存在?"}
UsernameExists --> |是| ReturnError1["返回错误: 用户名已存在"]
UsernameExists --> |否| CheckEmail["检查邮箱是否已存在"]
CheckEmail --> EmailExists{"邮箱已存在?"}
EmailExists --> |是| ReturnError2["返回错误: 邮箱已被注册"]
EmailExists --> |否| HashPassword["加密密码"]
HashPassword --> CreateAvatar["确定头像URL"]
CreateAvatar --> CreateUser["创建用户(角色=user)"]
CreateUser --> GenerateToken["生成JWT Token"]
GenerateToken --> ReturnSuccess["返回用户信息和Token"]
ReturnError1 --> End([结束])
ReturnError2 --> End
ReturnSuccess --> End
```
**图示来源**
- [user_service.go](file://internal/service/user_service.go)
**本节来源**
- [user_service.go](file://internal/service/user_service.go)
## 依赖分析
系统各组件之间存在明确的依赖关系。`auth.go`依赖于`pkg/auth/jwt.go`提供的JWT服务进行令牌验证。`user_service.go`依赖于`repository/user_repository.go`进行数据库操作,并依赖于`pkg/auth/password.go`进行密码加密。`rbac_model.conf`被Casbin引擎读取用于定义权限模型。这些依赖关系确保了系统的模块化和可维护性。
```mermaid
graph TD
A[auth.go] --> B[jwt.go]
C[user_service.go] --> D[user_repository.go]
C --> E[password.go]
F[rbac_model.conf] --> G[Casbin引擎]
```
**图示来源**
- [auth.go](file://internal/middleware/auth.go)
- [user_service.go](file://internal/service/user_service.go)
- [user_repository.go](file://internal/repository/user_repository.go)
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
**本节来源**
- [auth.go](file://internal/middleware/auth.go)
- [user_service.go](file://internal/service/user_service.go)
- [user_repository.go](file://internal/repository/user_repository.go)
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
## 性能考虑
系统的权限控制设计考虑了性能因素。JWT令牌在客户端存储减少了服务器的认证开销。Casbin的权限检查算法经过优化能够快速匹配权限策略。数据库查询使用了适当的索引`casbin_rule`表上的`ptype``v0``v1`索引提高了权限检查的效率。对于高并发场景可以考虑将权限策略缓存到Redis中进一步提高性能。
## 故障排除指南
当遇到权限相关问题时,可以按照以下步骤进行排查:
1. 检查JWT令牌是否正确生成和传递。
2. 验证`rbac_model.conf`文件的语法是否正确。
3. 检查数据库中的`casbin_rule`表是否包含正确的权限策略。
4. 确认用户的角色是否正确分配。
5. 查看系统日志,寻找权限拒绝的记录。
**本节来源**
- [auth.go](file://internal/middleware/auth.go)
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
- [audit_log.go](file://internal/model/audit_log.go)
## 结论
本系统采用基于Casbin的RBAC权限模型通过`rbac_model.conf`文件定义权限策略使用JWT认证中间件进行身份验证。该设计提供了灵活、可扩展的权限控制机制能够满足不同场景下的权限管理需求。通过合理配置权限策略可以实现细粒度的访问控制确保系统的安全性。

View File

@@ -1,326 +0,0 @@
# 认证与授权
<cite>
**本文档引用的文件**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
- [jwt.go](file://pkg/auth/jwt.go)
- [manager.go](file://pkg/auth/manager.go)
- [auth.go](file://internal/middleware/auth.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [token.go](file://internal/model/token.go)
- [token_service.go](file://internal/service/token_service.go)
- [user_service.go](file://internal/service/user_service.go)
- [user.go](file://internal/model/user.go)
- [config.go](file://pkg/config/config.go)
- [routes.go](file://internal/handler/routes.go)
- [audit_log.go](file://internal/model/audit_log.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构概述](#架构概述)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排除指南](#故障排除指南)
9. [结论](#结论)
## 简介
CarrotSkin项目实现了一套完整的认证与授权安全机制采用JWTJSON Web Token进行用户认证并结合Casbin实现基于角色的访问控制RBAC。该系统为用户提供安全的注册、登录、密码重置功能同时通过中间件对API请求进行权限验证。本文档详细说明了系统的安全架构、JWT认证流程、Casbin权限模型配置以及认证中间件的实现细节。
## 项目结构
CarrotSkin项目的认证与授权相关代码分布在多个目录中形成了清晰的分层架构。核心安全功能主要集中在`pkg/auth`包中,而具体的业务逻辑则分布在`internal`目录下的各个模块。
```mermaid
graph TB
subgraph "配置"
config[configs/casbin/rbac_model.conf]
end
subgraph "内部处理"
handler[internal/handler]
middleware[internal/middleware]
model[internal/model]
service[internal/service]
repository[internal/repository]
end
subgraph "公共包"
auth[pkg/auth]
config_pkg[pkg/config]
database[pkg/database]
redis[pkg/redis]
end
config --> auth
auth --> middleware
middleware --> handler
handler --> service
service --> repository
repository --> database
service --> redis
```
**Diagram sources**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
- [jwt.go](file://pkg/auth/jwt.go)
- [auth.go](file://internal/middleware/auth.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
**Section sources**
- [project_structure](file://project_structure)
## 核心组件
CarrotSkin项目的认证与授权系统由多个核心组件构成包括JWT服务、认证中间件、Casbin权限管理器、用户服务和令牌服务。这些组件协同工作确保系统的安全性。
**Section sources**
- [jwt.go](file://pkg/auth/jwt.go)
- [auth.go](file://internal/middleware/auth.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [user_service.go](file://internal/service/user_service.go)
- [token_service.go](file://internal/service/token_service.go)
## 架构概述
CarrotSkin的认证与授权架构采用分层设计从下到上的层次包括配置层、认证服务层、中间件层、处理器层和路由层。这种设计实现了关注点分离提高了代码的可维护性和可测试性。
```mermaid
graph TD
A[客户端] --> B[HTTP请求]
B --> C{路由}
C --> D[/认证路由\n无需JWT/]
C --> E[/受保护路由\n需要JWT/]
D --> F[auth_handler]
E --> G[AuthMiddleware]
G --> H{验证JWT}
H --> |有效| I[业务处理器]
H --> |无效| J[返回401]
I --> K[service]
K --> L[repository]
L --> M[(数据库)]
G --> N[Casbin权限检查]
N --> |允许| I
N --> |拒绝| O[返回403]
P[JWT配置] --> Q[jwt.go]
Q --> R[AuthMiddleware]
S[Casbin配置] --> T[CasbinRule]
T --> N
```
**Diagram sources**
- [routes.go](file://internal/handler/routes.go)
- [auth.go](file://internal/middleware/auth.go)
- [config.go](file://pkg/config/config.go)
- [audit_log.go](file://internal/model/audit_log.go)
## 详细组件分析
### JWT认证服务分析
JWT认证服务是CarrotSkin安全机制的核心负责生成和验证JWT令牌。该服务实现了标准的JWT功能包括令牌的签发、验证和声明管理。
#### JWT服务类图
```mermaid
classDiagram
class JWTService {
+string secretKey
+int expireHours
+GenerateToken(userID int64, username string, role string) (string, error)
+ValidateToken(tokenString string) (*Claims, error)
}
class Claims {
+int64 UserID
+string Username
+string Role
+jwt.RegisteredClaims
}
class JWTManager {
-static *JWTService jwtServiceInstance
-static sync.Once once
+Init(cfg JWTConfig) error
+GetJWTService() (*JWTService, error)
+MustGetJWTService() *JWTService
}
JWTManager --> JWTService : "创建"
JWTService --> Claims : "包含"
```
**Diagram sources**
- [jwt.go](file://pkg/auth/jwt.go)
- [manager.go](file://pkg/auth/manager.go)
**Section sources**
- [jwt.go](file://pkg/auth/jwt.go)
- [manager.go](file://pkg/auth/manager.go)
### 认证中间件分析
认证中间件负责在请求处理流程中验证JWT令牌的有效性。它拦截所有受保护的API请求确保只有携带有效令牌的请求才能访问受保护的资源。
#### 认证中间件流程图
```mermaid
flowchart TD
Start([开始]) --> GetHeader["获取Authorization头"]
GetHeader --> HeaderEmpty{"头为空?"}
HeaderEmpty --> |是| Return401Unauthorized["返回401未授权"]
HeaderEmpty --> |否| ParseHeader["解析Bearer格式"]
ParseHeader --> FormatValid{"格式有效?"}
FormatValid --> |否| Return401InvalidFormat["返回401无效格式"]
FormatValid --> |是| ExtractToken["提取Token"]
ExtractToken --> ValidateToken["验证Token"]
ValidateToken --> TokenValid{"Token有效?"}
TokenValid --> |否| Return401InvalidToken["返回401无效Token"]
TokenValid --> |是| StoreUserInfo["将用户信息存入上下文"]
StoreUserInfo --> Next["调用下一个处理器"]
Next --> End([结束])
Return401Unauthorized --> End
Return401InvalidFormat --> End
Return401InvalidToken --> End
```
**Diagram sources**
- [auth.go](file://internal/middleware/auth.go)
**Section sources**
- [auth.go](file://internal/middleware/auth.go)
### Casbin RBAC权限模型分析
CarrotSkin使用Casbin实现基于角色的访问控制RBAC模型通过配置文件定义权限策略实现灵活的权限管理。
#### Casbin RBAC模型配置
```conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
```
**Diagram sources**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
**Section sources**
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
### 用户认证流程分析
用户认证流程涵盖了从注册、登录到密码重置的完整生命周期,确保用户身份的安全管理。
#### 用户认证流程序列图
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "AuthHandler"
participant Service as "UserService"
participant JWT as "JWTService"
participant DB as "数据库"
Client->>Handler : POST /api/v1/auth/register
Handler->>Service : RegisterUser()
Service->>DB : 检查用户名/邮箱是否存在
DB-->>Service : 返回检查结果
Service->>JWT : HashPassword()
Service->>DB : 创建用户记录
DB-->>Service : 用户对象
Service->>JWT : GenerateToken()
JWT-->>Service : JWT Token
Service-->>Handler : 用户和Token
Handler-->>Client : 200 OK {token, userInfo}
Client->>Handler : POST /api/v1/auth/login
Handler->>Service : LoginUser()
Service->>DB : 查找用户(用户名/邮箱)
DB-->>Service : 用户对象
Service->>JWT : CheckPassword()
Service->>JWT : GenerateToken()
JWT-->>Service : JWT Token
Service-->>Handler : 用户和Token
Handler-->>Client : 200 OK {token, userInfo}
```
**Diagram sources**
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [user_service.go](file://internal/service/user_service.go)
- [jwt.go](file://pkg/auth/jwt.go)
**Section sources**
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [user_service.go](file://internal/service/user_service.go)
## 依赖分析
CarrotSkin的认证与授权系统依赖于多个外部包和内部组件形成了复杂的依赖关系网络。
```mermaid
graph TD
A[pkg/auth] --> B[github.com/golang-jwt/jwt/v5]
A --> C[pkg/config]
D[internal/middleware] --> A
D --> E[github.com/gin-gonic/gin]
F[internal/handler] --> D
F --> G[internal/service]
G --> A
G --> H[pkg/database]
G --> I[pkg/redis]
J[pkg/config] --> K[github.com/spf13/viper]
J --> L[github.com/joho/godotenv]
H --> M[gorm.io/gorm]
I --> N[github.com/redis/go-redis]
```
**Diagram sources**
- [go.mod](file://go.mod)
- [jwt.go](file://pkg/auth/jwt.go)
- [auth.go](file://internal/middleware/auth.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
**Section sources**
- [go.mod](file://go.mod)
- [go.sum](file://go.sum)
## 性能考虑
CarrotSkin的认证与授权系统在设计时考虑了性能因素通过多种机制确保系统的高效运行。
1. **JWT令牌验证**JWT令牌的验证是无状态的不需要查询数据库大大提高了验证速度。
2. **Redis缓存**系统使用Redis存储验证码等临时数据减少了数据库的压力。
3. **连接池**数据库和Redis都使用了连接池避免了频繁创建和销毁连接的开销。
4. **异步操作**某些非关键操作如清理多余令牌使用goroutine异步执行不影响主流程性能。
5. **批量操作**:在清理多余令牌时,使用批量删除操作,减少了数据库交互次数。
**Section sources**
- [token_service.go](file://internal/service/token_service.go)
- [repository/token_repository.go](file://internal/repository/token_repository.go)
- [config.go](file://pkg/config/config.go)
## 故障排除指南
当遇到认证与授权相关的问题时,可以参考以下常见问题的解决方案:
1. **401 Unauthorized错误**
- 检查请求头中是否包含`Authorization`
- 确认`Authorization`头的格式是否为`Bearer <token>`
- 验证JWT令牌是否已过期
- 检查JWT密钥配置是否正确
2. **403 Forbidden错误**
- 检查用户角色是否有权限访问该资源
- 验证Casbin策略配置是否正确
- 确认请求的资源和操作是否匹配策略
3. **登录失败**
- 检查用户名/邮箱和密码是否正确
- 确认用户账户状态是否正常(未被禁用)
- 检查数据库连接是否正常
4. **令牌刷新失败**
- 确认旧令牌是否有效
- 检查客户端令牌是否匹配
- 验证用户是否有权限选择指定的角色
**Section sources**
- [auth.go](file://internal/middleware/auth.go)
- [auth_handler.go](file://internal/handler/auth_handler.go)
- [token_service.go](file://internal/service/token_service.go)
## 结论
CarrotSkin项目实现了一套完整且安全的认证与授权机制通过JWT和Casbin的结合提供了灵活的用户身份验证和细粒度的权限控制。系统的分层架构使得各个组件职责清晰易于维护和扩展。对于初学者系统提供了清晰的API文档和错误处理机制对于经验丰富的开发者系统提供了扩展权限模型和集成其他认证方式的可能性。整体而言CarrotSkin的安全机制设计合理能够有效保护系统资源为用户提供安全可靠的服务。

View File

@@ -1,276 +0,0 @@
# 部署
<cite>
**本文档引用的文件**
- [start.sh](file://start.sh)
- [run.sh](file://run.sh)
- [scripts/check-env.sh](file://scripts/check-env.sh)
- [scripts/dev.sh](file://scripts/dev.sh)
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
- [internal/handler/routes.go](file://internal/handler/routes.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [环境准备](#环境准备)
4. [部署流程](#部署流程)
5. [启动脚本详解](#启动脚本详解)
6. [生产环境配置](#生产环境配置)
7. [健康检查与监控](#健康检查与监控)
8. [日志管理](#日志管理)
9. [高级部署策略](#高级部署策略)
10. [故障排除](#故障排除)
## 简介
CarrotSkin 是一个为 Minecraft 皮肤站设计的后端服务,提供用户管理、材质上传下载、档案管理等功能。本部署文档详细介绍了如何在生产环境中部署和运行 CarrotSkin 服务,包括使用 `start.sh``run.sh` 脚本的完整流程、环境变量配置、依赖项管理以及运维监控等方面的内容。文档既为初学者提供了简单的部署步骤,也为经验丰富的开发者提供了高可用性和可扩展性部署的高级策略。
## 项目结构
CarrotSkin 项目采用标准的 Go 项目结构,主要包含以下目录:
- `configs/`:配置文件目录,包含 Casbin 权限模型等配置
- `internal/`:核心业务逻辑,包括处理器、中间件、模型、仓库和服务
- `pkg/`可重用的包如数据库、Redis、日志、配置管理等
- `scripts/`:脚本目录,包含环境检查和开发环境启动脚本
- 根目录:包含主启动脚本 `start.sh``run.sh` 和 Go 模块文件
项目通过环境变量进行配置,不依赖传统的 YAML 配置文件,这使得部署更加灵活,特别适合容器化环境。
**Section sources**
- [start.sh](file://start.sh)
- [run.sh](file://run.sh)
- [scripts/check-env.sh](file://scripts/check-env.sh)
- [scripts/dev.sh](file://scripts/dev.sh)
## 环境准备
在部署 CarrotSkin 服务之前,需要确保以下依赖项已正确安装和配置:
1. **Go 语言环境**:项目需要 Go 1.23.0 或更高版本。可以通过 `go version` 命令检查当前 Go 版本。
2. **PostgreSQL 数据库**:服务依赖 PostgreSQL 作为主数据库。需要确保数据库服务已启动,并创建了相应的数据库和用户。
3. **Redis 服务**:用于缓存和会话管理。需要确保 Redis 服务正在运行。
4. **RustFS (S3 兼容对象存储)**:用于存储用户上传的皮肤和头像文件。需要配置一个 S3 兼容的对象存储服务。
5. **swag 工具**:用于生成 Swagger API 文档。如果未安装,`run.sh` 脚本会自动安装。
对于生产环境,建议使用 Docker 或 Kubernetes 来管理这些依赖服务,以确保环境的一致性和可移植性。
**Section sources**
- [go.mod](file://go.mod)
- [pkg/config/config.go](file://pkg/config/config.go)
- [scripts/check-env.sh](file://scripts/check-env.sh)
## 部署流程
CarrotSkin 提供了多种部署方式,从简单的本地部署到复杂的生产环境部署。
### 基础部署步骤
1. **克隆代码库**:将 CarrotSkin 后端代码克隆到目标服务器。
2. **安装依赖**:运行 `go mod tidy` 安装所有 Go 依赖包。
3. **配置环境变量**:根据 `start.sh` 脚本中的示例,设置所有必需的环境变量。
4. **生成 API 文档**:运行 `swag init` 生成 Swagger API 文档。
5. **启动服务**:执行 `run.sh``start.sh` 脚本启动服务。
### 使用 run.sh 脚本
`run.sh` 脚本是推荐的开发和部署方式,它自动化了大部分部署步骤:
- 自动检查并安装 `swag` 工具
- 生成最新的 Swagger API 文档
- 启动 Go 服务
```bash
./run.sh
```
### 使用 start.sh 脚本
`start.sh` 脚本直接设置了所有环境变量并启动服务,适合在环境变量管理不便的场景下使用。
```bash
./start.sh
```
**Section sources**
- [run.sh](file://run.sh)
- [start.sh](file://start.sh)
- [scripts/dev.sh](file://scripts/dev.sh)
## 启动脚本详解
CarrotSkin 提供了多个启动脚本以适应不同的使用场景。
### start.sh 脚本
`start.sh` 脚本是一个自包含的启动脚本,它在脚本内部定义了所有必需的环境变量,然后直接启动服务。这种方式简单直接,但不推荐用于生产环境,因为敏感信息(如数据库密码)直接暴露在脚本中。
**Section sources**
- [start.sh](file://start.sh)
### run.sh 脚本
`run.sh` 脚本是一个更智能的启动脚本,它分为三个阶段:
1. **检查 swag 工具**:确保 `swag` 命令可用,如果不存在则自动安装。
2. **生成 Swagger 文档**:运行 `swag init` 命令生成 API 文档,确保 API 文档与代码同步。
3. **启动服务器**:最后启动 Go 服务。
这个脚本的优势在于它确保了 API 文档的实时性,并且不包含敏感的环境变量。
**Section sources**
- [run.sh](file://run.sh)
### check-env.sh 脚本
`scripts/check-env.sh` 是一个环境检查脚本,用于验证必需的环境变量是否已正确设置。它会检查 `.env` 文件是否存在并验证关键变量如数据库连接、RustFS 凭证、JWT 密钥)是否已配置。此外,它还会检查 JWT 密钥的长度和是否使用了默认值,提供安全建议。
**Section sources**
- [scripts/check-env.sh](file://scripts/check-env.sh)
## 生产环境配置
在生产环境中部署 CarrotSkin 时,必须进行严格的安全和性能配置。
### 环境变量配置
所有配置都通过环境变量进行。关键的环境变量包括:
- **数据库配置**`DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`
- **Redis 配置**`REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`
- **对象存储配置**`RUSTFS_ENDPOINT`, `RUSTFS_ACCESS_KEY`, `RUSTFS_SECRET_KEY`
- **安全配置**`JWT_SECRET`(必须使用至少 32 位的强随机字符串)
### 配置管理
项目使用 Viper 库从环境变量中读取配置。`pkg/config/config.go` 文件定义了所有配置项的结构和默认值。生产环境中应避免使用脚本中硬编码的默认值,而是通过环境变量或配置管理工具(如 Kubernetes ConfigMap/Secret来设置。
```mermaid
flowchart TD
A[环境变量] --> B[pkg/config/config.go]
B --> C[应用配置]
C --> D[数据库连接]
C --> E[Redis连接]
C --> F[RustFS连接]
C --> G[JWT配置]
```
**Diagram sources**
- [pkg/config/config.go](file://pkg/config/config.go)
**Section sources**
- [pkg/config/config.go](file://pkg/config/config.go)
- [start.sh](file://start.sh)
## 健康检查与监控
CarrotSkin 内置了健康检查功能,便于集成到监控系统中。
### 健康检查端点
服务提供了一个 `/health` 的健康检查端点,返回简单的 JSON 响应:
```json
{
"status": "ok",
"message": "CarrotSkin API is running"
}
```
该端点由 `internal/handler/swagger.go` 中的 `HealthCheck` 函数实现,无需认证即可访问,适合用于负载均衡器和容器编排平台的健康探测。
### 监控建议
建议将以下指标纳入监控系统:
- HTTP 请求延迟和错误率
- 数据库连接池使用情况
- Redis 内存使用和命中率
- 对象存储的读写性能
```mermaid
sequenceDiagram
participant 监控系统
participant CarrotSkin服务
participant 数据库
participant Redis
participant 对象存储
监控系统->>CarrotSkin服务 : GET /health
CarrotSkin服务->>数据库 : 检查连接
CarrotSkin服务->>Redis : 检查连接
CarrotSkin服务->>对象存储 : 检查连接
CarrotSkin服务-->>监控系统 : 返回健康状态
```
**Diagram sources**
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
- [internal/handler/routes.go](file://internal/handler/routes.go)
**Section sources**
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
## 日志管理
CarrotSkin 使用 Zap 日志库进行日志记录,提供了结构化日志输出。
### 日志配置
日志行为由以下环境变量控制:
- `LOG_LEVEL`日志级别debug, info, warn, error
- `LOG_FORMAT`日志格式json 或 console
- `LOG_OUTPUT`:日志输出位置(文件路径或 stdout
### 日志轮转
日志文件支持自动轮转,配置包括最大文件大小、备份数量和最大保留天数。日志文件默认存储在 `logs/app.log`
```mermaid
flowchart TD
A[应用事件] --> B[pkg/logger/logger.go]
B --> C{日志级别}
C --> |高于配置级别| D[忽略]
C --> |低于或等于| E[格式化]
E --> F{输出目标}
F --> |stdout| G[控制台输出]
F --> |文件| H[写入日志文件]
H --> I[日志轮转]
```
**Diagram sources**
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
**Section sources**
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
- [pkg/config/config.go](file://pkg/config/config.go)
## 高级部署策略
对于生产环境,建议采用以下高级部署策略。
### Docker 部署
虽然项目未提供 Dockerfile但可以轻松创建。Docker 部署的优势包括环境隔离、版本控制和易于扩展。
```dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod tidy
RUN go build -o carrotskin cmd/server/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/carrotskin .
COPY --from=builder /app/docs docs
EXPOSE 8080
CMD ["./carrotskin"]
```
### CI/CD 集成
建议将部署流程集成到 CI/CD 管道中:
1. 代码推送触发 CI 构建
2. 运行单元测试和集成测试
3. 构建 Docker 镜像
4. 推送镜像到镜像仓库
5. 在目标环境部署新版本
### 高可用性部署
对于高可用性要求,建议:
- 使用 Kubernetes 部署多个服务实例
- 配置负载均衡器分发流量
- 使用外部的 PostgreSQL 集群和 Redis 集群
- 实现蓝绿部署或金丝雀发布
## 故障排除
### 常见问题
- **服务无法启动**:检查 `check-env.sh` 脚本,确保所有必需的环境变量已设置。
- **数据库连接失败**:验证数据库主机、端口、用户名和密码是否正确。
- **JWT 密钥警告**:生产环境中必须更改默认的 JWT 密钥。
- **Swagger 文档缺失**:确保已运行 `swag init` 命令。
### 调试建议
- 查看日志文件 `logs/app.log` 获取详细错误信息
- 使用 `curl http://localhost:8080/health` 检查服务健康状态
- 检查依赖服务数据库、Redis、对象存储是否正常运行
**Section sources**
- [scripts/check-env.sh](file://scripts/check-env.sh)
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
- [internal/handler/swagger.go](file://internal/handler/swagger.go)

View File

@@ -1,331 +0,0 @@
# JWT配置
<cite>
**本文引用的文件**
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go)
- [pkg/auth/manager.go](file://pkg/auth/manager.go)
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/config/manager.go](file://pkg/config/manager.go)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
- [internal/service/user_service.go](file://internal/service/user_service.go)
- [scripts/check-env.sh](file://scripts/check-env.sh)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件聚焦于CarrotSkin项目的JWT认证令牌配置系统性说明以下内容
- jwt.secret与expire_hours两个核心配置项的作用与默认值
- 如何通过环境变量JWT_SECRET与JWT_EXPIRE_HOURS进行安全覆盖
- 基于JWTConfig结构体与认证服务代码的安全最佳实践
- 密钥生成建议与有效期设置策略
## 项目结构
围绕JWT配置的相关文件分布如下
- 配置加载与默认值pkg/config/config.go、pkg/config/manager.go
- JWT服务与管理器pkg/auth/jwt.go、pkg/auth/manager.go
- 认证处理器与服务层集成internal/handler/auth_handler.go、internal/service/user_service.go
- 环境变量检查脚本scripts/check-env.sh
```mermaid
graph TB
subgraph "配置层"
CFG["pkg/config/config.go<br/>加载与默认值"]
CMGR["pkg/config/manager.go<br/>配置单例管理"]
end
subgraph "认证层"
JCFG["pkg/auth/jwt.go<br/>JWT服务与声明"]
JMGR["pkg/auth/manager.go<br/>JWT服务单例管理"]
end
subgraph "业务层"
AH["internal/handler/auth_handler.go<br/>登录/注册接口"]
US["internal/service/user_service.go<br/>登录/注册业务"]
end
subgraph "工具"
ENV["scripts/check-env.sh<br/>环境变量检查"]
end
CFG --> CMGR
CMGR --> JMGR
JCFG --> JMGR
AH --> US
AH --> JMGR
US --> JCFG
ENV --> CFG
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
## 核心组件
- JWTConfig结构体
- 字段Secret密钥、ExpireHours过期小时数
- 默认值ExpireHours默认168小时7天
- 环境变量映射JWT_SECRET、JWT_EXPIRE_HOURS
- JWTService
- 作用生成与验证JWT令牌声明包含用户ID、用户名、角色及标准声明签发时间、过期时间等
- 过期时间由expireHours决定默认168小时
- JWT服务管理器
- 提供Init(cfg)一次性初始化GetJWTService()/MustGetJWTService()获取全局实例
- 认证服务集成
- 登录/注册成功后通过JWTService生成令牌并返回给客户端
- 环境变量检查
- 脚本会校验JWT_SECRET长度与默认值提示
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L67-L71)
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
- [scripts/check-env.sh](file://scripts/check-env.sh#L58-L77)
## 架构总览
下图展示JWT配置从环境变量到服务使用的端到端流程。
```mermaid
sequenceDiagram
participant ENV as "环境变量"
participant CFG as "配置加载(config.Load)"
participant CMGR as "配置管理器(config.Init)"
participant JMGR as "JWT管理器(auth.Init)"
participant Jsvc as "JWT服务(auth.JWTService)"
participant H as "认证处理器(handler.Login/Register)"
participant S as "业务服务(service.LoginUser/RegisterUser)"
ENV-->>CFG : "JWT_SECRET/JWT_EXPIRE_HOURS"
CFG-->>CMGR : "加载并解析配置(Config)"
CMGR-->>JMGR : "传入JWTConfig(Secret, ExpireHours)"
JMGR-->>Jsvc : "创建JWTService实例"
H->>S : "调用登录/注册业务"
S->>Jsvc : "生成JWT令牌"
Jsvc-->>S : "返回token"
S-->>H : "返回token与用户信息"
H-->>Client : "HTTP响应(含token)"
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L32-L53)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
## 详细组件分析
### JWTConfig与默认值
- 结构体定义
- Secret字符串用于签名与验证JWT
- ExpireHours整数令牌有效期小时默认1687天
- 默认值来源
- 通过Viper设置默认值确保未提供环境变量时仍能运行
- 环境变量映射
- JWT_SECRET → jwt.secret
- JWT_EXPIRE_HOURS → jwt.expire_hours
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L67-L71)
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)
### JWTService与Claims
- JWTService
- 字段secretKey、expireHours
- 方法GenerateToken、ValidateToken
- Claims
- 包含用户ID、用户名、角色以及标准声明签发时间、过期时间、生效时间、发行者等
- 令牌生成
- 使用HS256算法签名过期时间基于expireHours计算
- 令牌验证
- 使用同一secretKey进行验证若签名不符或过期则报错
```mermaid
classDiagram
class JWTService {
-string secretKey
-int expireHours
+GenerateToken(userID, username, role) string,error
+ValidateToken(tokenString) Claims,error
}
class Claims {
+int64 UserID
+string Username
+string Role
+RegisteredClaims
}
JWTService --> Claims : "生成/验证"
```
图表来源
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
章节来源
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
### JWT服务管理器
- Init(cfg)
- 仅初始化一次使用sync.Once保证幂等
- 将cfg中的Secret与ExpireHours注入JWTService
- GetJWTService/MustGetJWTService
- 提供全局唯一实例访问未初始化时报错或panic
```mermaid
flowchart TD
Start(["调用 auth.Init(cfg)"]) --> Once{"是否已初始化?"}
Once --> |否| Create["创建JWTService实例<br/>注入Secret/ExpireHours"]
Once --> |是| Skip["跳过初始化"]
Create --> Done(["完成"])
Skip --> Done
```
图表来源
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
章节来源
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
### 认证服务与JWT集成
- 登录/注册成功后业务层调用JWTService生成令牌
- 处理器层接收令牌并返回给客户端
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "Handler(Login/Register)"
participant S as "Service(LoginUser/RegisterUser)"
participant J as "JWTService"
C->>H : "POST /api/v1/auth/login"
H->>S : "LoginUser(jwtService, ...)"
S->>J : "GenerateToken(userID, username, role)"
J-->>S : "token"
S-->>H : "user + token"
H-->>C : "200 OK + token"
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L32-L53)
章节来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L32-L53)
### 环境变量覆盖与安全检查
- 环境变量映射
- JWT_SECRET → jwt.secret
- JWT_EXPIRE_HOURS → jwt.expire_hours
- 默认值
- ExpireHours默认1687天
- 安全检查
- 脚本会检查JWT_SECRET长度是否小于32字符并提示使用默认密钥的风险
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
- [scripts/check-env.sh](file://scripts/check-env.sh#L58-L77)
## 依赖关系分析
- 配置层
- config.Load负责从.env与环境变量加载配置设置默认值并映射JWT相关键
- config.Init提供全局配置单例
- 认证层
- auth.Init接收JWTConfig创建JWTService实例
- auth.GetJWTService/MustGetJWTService提供全局访问
- 业务层
- handler.Login/Register通过MustGetJWTService获取JWTService
- service.LoginUser/RegisterUser调用JWTService生成令牌
```mermaid
graph LR
CFG["config.Load"] --> CMGR["config.Init/GetConfig"]
CMGR --> JMGR["auth.Init"]
JMGR --> Jsvc["auth.JWTService"]
AH["handler.Login/Register"] --> Jsvc
US["service.LoginUser/RegisterUser"] --> Jsvc
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
## 性能考量
- 令牌生成与验证均为内存操作,开销极低
- 过期时间越长,客户端缓存令牌的时间越久,减少鉴权次数;但会增加泄露风险
- 建议在移动端或易泄露设备上缩短expire_hours在可信环境适当延长
## 故障排查指南
- JWT服务未初始化
- 现象调用GetJWTService/MustGetJWTService时报错或panic
- 排查确认已在应用启动阶段调用auth.Init(cfg)
- 令牌无效
- 现象ValidateToken返回错误
- 排查确认使用相同的JWT_SECRET检查网络传输是否篡改核对过期时间
- 环境变量未生效
- 现象JWT_EXPIRE_HOURS未按预期
- 排查:确认环境变量名正确且已导出;检查.env文件路径与加载顺序
章节来源
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L26-L41)
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L55-L71)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
## 结论
- jwt.secret用于签名与验证JWT必须保密且足够随机
- expire_hours控制令牌有效期默认168小时7天可通过JWT_EXPIRE_HOURS覆盖
- 通过JWT_SECRET与JWT_EXPIRE_HOURS实现安全可控的令牌策略
- 建议在生产环境使用长随机密钥,并根据业务场景调整有效期
## 附录
### 安全最佳实践
- 密钥生成建议
- 使用足够长度的随机字符串作为JWT_SECRET建议≥32字符
- 避免使用默认密钥或弱口令
- 有效期设置策略
- 移动端/公共设备较短有效期如24-72小时
- 可信服务间可适当延长如7天
- 环境变量管理
- 在部署时通过环境变量覆盖,避免硬编码
- 使用脚本检查密钥长度与默认值风险
章节来源
- [scripts/check-env.sh](file://scripts/check-env.sh#L58-L77)
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)

View File

@@ -1,331 +0,0 @@
# Redis配置
<cite>
**本文引用的文件**
- [pkg/redis/redis.go](file://pkg/redis/redis.go)
- [pkg/redis/manager.go](file://pkg/redis/manager.go)
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/config/manager.go](file://pkg/config/manager.go)
- [cmd/server/main.go](file://cmd/server/main.go)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
- [internal/service/verification_service.go](file://internal/service/verification_service.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向CarrotSkin项目的开发者系统性梳理Redis缓存系统的配置与使用方式重点说明以下内容
- RedisConfig结构体中的各配置项host、port、password、database、pool_size的作用与默认值
- pool_size参数对并发性能的影响机制
- 如何通过REDIS_*环境变量动态覆盖配置
- 结合RedisConfig结构体与连接初始化逻辑给出多环境部署建议与连接池大小调优方法
## 项目结构
Redis配置与使用涉及如下关键模块
- 配置加载与默认值pkg/config
- Redis客户端封装与连接初始化pkg/redis
- 应用启动流程cmd/server/main.go
- 业务使用示例internal/handler与internal/service
```mermaid
graph TB
subgraph "配置层"
CFG["pkg/config/config.go<br/>RedisConfig结构体与默认值"]
CM["pkg/config/manager.go<br/>配置单例管理"]
end
subgraph "Redis层"
RC["pkg/redis/redis.go<br/>Client封装与New初始化"]
RM["pkg/redis/manager.go<br/>Init/GetClient/MustGetClient"]
end
subgraph "应用层"
MAIN["cmd/server/main.go<br/>启动时初始化Redis"]
AUTH["internal/handler/auth_handler.go<br/>使用MustGetClient()"]
SVC["internal/service/verification_service.go<br/>验证码场景使用Redis"]
end
CFG --> CM
CM --> MAIN
MAIN --> RM
RM --> RC
AUTH --> RM
SVC --> RC
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
- [cmd/server/main.go](file://cmd/server/main.go#L52-L61)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
- [cmd/server/main.go](file://cmd/server/main.go#L52-L61)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
## 核心组件
- RedisConfig结构体
- 字段host、port、password、database、pool_size
- 默认值:由配置加载器在未显式提供时设置
- 配置加载与环境变量覆盖
- 默认值设置、环境变量前缀、映射与覆盖逻辑
- Redis客户端封装与初始化
- Client封装、New初始化、Ping连通性校验、连接池大小
- 单例初始化与获取接口Init/GetClient/MustGetClient
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
## 架构总览
下图展示Redis配置与初始化在应用生命周期中的位置与交互。
```mermaid
sequenceDiagram
participant Main as "main.go"
participant CfgMgr as "config.Manager"
participant Cfg as "config.Config"
participant RedisMgr as "redis.Manager"
participant RedisClient as "redis.Client"
Main->>CfgMgr : Init()
CfgMgr-->>Main : 配置实例
Main->>RedisMgr : Init(RedisConfig, Logger)
RedisMgr->>RedisClient : New(cfg, logger)
RedisClient->>RedisClient : Ping()连通性校验
RedisMgr-->>Main : 初始化完成
Main-->>Main : 启动HTTP服务
```
图表来源
- [cmd/server/main.go](file://cmd/server/main.go#L27-L61)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
## 详细组件分析
### RedisConfig结构体与默认值
- 字段定义与含义
- hostRedis服务器地址默认值由配置加载器设置
- portRedis服务器端口默认值由配置加载器设置
- passwordRedis认证密码默认值由配置加载器设置
- databaseRedis数据库索引默认值由配置加载器设置
- pool_sizeRedis连接池大小默认值由配置加载器设置
- 默认值来源
- 配置加载器在未显式提供时设置默认值,确保应用在最小配置下仍可运行
- 环境变量映射
- REDIS_HOST、REDIS_PORT、REDIS_PASSWORD、REDIS_DATABASE
- REDIS_POOL_SIZE用于覆盖连接池大小
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
### 连接初始化与Ping校验
- 初始化流程
- 通过redis.NewClient创建客户端使用RedisConfig中的host/port/password/database/pool_size
- 使用Ping进行连通性校验失败则返回错误
- 初始化成功后记录日志包含host/port/database
- 并发与线程安全
- Redis初始化采用once.Do保证全局仅初始化一次
- GetClient/MustGetClient提供线程安全的获取方式
```mermaid
flowchart TD
Start(["进入Init(cfg, logger)"]) --> Once["once.Do执行"]
Once --> NewClient["创建redis.Client(options)"]
NewClient --> Ping["Ping()连通性校验"]
Ping --> Ok{"Ping成功"}
Ok --> |否| Err["返回错误"]
Ok --> |是| Log["记录连接成功日志"]
Log --> Done(["返回Client实例"])
Err --> Done
```
图表来源
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
章节来源
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
### 环境变量动态覆盖机制
- 环境变量前缀与映射
- 配置加载器设置环境变量前缀为CARROTSKIN并将redis.*字段绑定到REDIS_*环境变量
- 动态覆盖逻辑
- 在解析默认值与环境变量后额外处理REDIS_POOL_SIZE以覆盖连接池大小
- 使用建议
- 开发环境可使用较小pool_size生产环境根据QPS与并发峰值评估后设置
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
### 业务使用示例
- 验证码场景
- 发送验证码前检查频率限制键是否存在
- 将验证码写入Redis并设置过期时间
- 发送邮件失败时删除验证码键
- 登录/注册/重置密码
- Handler层通过MustGetClient()获取Redis客户端
- Service层调用Redis进行验证码校验与清理
```mermaid
sequenceDiagram
participant H as "auth_handler"
participant S as "verification_service"
participant R as "redis.Client"
H->>R : MustGetClient()
H->>S : SendVerificationCode(ctx, R, emailService, email, type)
S->>R : Exists(rateLimitKey)
alt 存在频率限制
S-->>H : 返回“发送过于频繁”
else 无频率限制
S->>R : Set(codeKey, code, expiration)
S->>R : Set(rateLimitKey, "1", rateLimit)
S-->>H : 发送成功
end
```
图表来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L31-L46)
章节来源
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
## 依赖关系分析
- 配置到Redis的依赖链
- config.Manager -> config.Config -> redis.Manager -> redis.Client
- 启动阶段依赖
- main.go在启动时顺序初始化配置、日志、数据库、JWT、Redis、对象存储、邮件服务
- 业务依赖
- Handler与Service通过redis.Manager提供的单例接口使用Redis
```mermaid
graph LR
CFG["config.Config"] --> RM["redis.Manager"]
RM --> RC["redis.Client"]
MAIN["cmd/server/main.go"] --> RM
AUTH["internal/handler/auth_handler.go"] --> RM
SVC["internal/service/verification_service.go"] --> RC
```
图表来源
- [cmd/server/main.go](file://cmd/server/main.go#L27-L61)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
章节来源
- [cmd/server/main.go](file://cmd/server/main.go#L27-L61)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
## 性能考量
- pool_size对并发性能的影响机制
- 连接池大小决定同时可用的底层TCP连接数
- 在高并发场景下若pool_size过小可能出现连接争用与等待导致延迟上升
- 若pool_size过大会增加Redis端的连接管理开销与资源占用
- 调优建议
- 基准测试在预生产环境模拟峰值QPS逐步提升pool_size并观察延迟与错误率
- 观察指标监控Redis连接数、等待队列长度、命令执行延迟
- 平衡策略在满足延迟目标的前提下尽量保持pool_size适中避免过度放大
- 与业务场景的匹配
- 验证码发送/校验属于短时高频写入场景适度增大pool_size有助于降低写入抖动
- 读多写少的场景可结合Pipeline/TxPipeline减少RTT进一步提升吞吐
[本节为通用性能讨论,不直接分析具体文件]
## 故障排查指南
- 初始化失败
- 症状启动时报Redis连接失败
- 排查要点确认REDIS_HOST/REDIS_PORT/REDIS_PASSWORD/REDIS_DATABASE是否正确检查网络连通性确认Redis服务状态
- 参考实现初始化时进行Ping校验失败返回错误
- 未初始化即使用
- 症状调用MustGetClient()时panic或GetClient()返回错误
- 排查要点确保在main中先调用redis.Init()再启动HTTP服务
- 频繁超时或错误
- 症状业务调用Redis出现超时或错误
- 排查要点检查pool_size是否过小确认Redis资源瓶颈优化业务逻辑减少阻塞操作
- 验证码发送过于频繁
- 症状:提示发送过于频繁
- 排查要点:检查频率限制键是否存在且未过期;确认业务逻辑正确清理
章节来源
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L31-L46)
- [cmd/server/main.go](file://cmd/server/main.go#L52-L61)
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
## 结论
- RedisConfig提供了简洁明确的配置项配合默认值与环境变量覆盖可在多环境中快速部署
- pool_size直接影响并发性能应结合业务负载与Redis资源进行调优
- 通过once.Do保证Redis客户端单例初始化配合MustGetClient/GetClient简化了业务侧使用
- 建议在生产环境启用合理的pool_size并持续监控连接池利用率与延迟指标
[本节为总结性内容,不直接分析具体文件]
## 附录
### 配置项与默认值对照表
- hostRedis主机地址默认值由配置加载器设置
- portRedis端口默认值由配置加载器设置
- passwordRedis认证密码默认值由配置加载器设置
- databaseRedis数据库索引默认值由配置加载器设置
- pool_sizeRedis连接池大小默认值由配置加载器设置
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
### 环境变量覆盖清单
- REDIS_HOST覆盖host
- REDIS_PORT覆盖port
- REDIS_PASSWORD覆盖password
- REDIS_DATABASE覆盖database
- REDIS_POOL_SIZE覆盖pool_size
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
### 多环境部署建议
- 开发环境
- 使用较小pool_size如默认值便于本地调试
- 通过REDIS_*环境变量快速切换Redis实例
- 预生产/生产环境
- 基于基准测试确定pool_size结合监控指标持续优化
- 使用独立database索引隔离不同环境数据
- 对password进行严格管理避免明文配置
[本节为通用建议,不直接分析具体文件]

View File

@@ -1,305 +0,0 @@
# 对象存储配置
<cite>
**本文引用的文件列表**
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/config/manager.go](file://pkg/config/manager.go)
- [pkg/storage/manager.go](file://pkg/storage/manager.go)
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
- [cmd/server/main.go](file://cmd/server/main.go)
- [start.sh](file://start.sh)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向开发者,系统性说明 CarrotSkin 项目与 RustFSS3 兼容)对象存储的集成配置。重点涵盖以下方面:
- 核心配置项 rustfs.endpoint、access_key、secret_key、use_ssl 的作用与含义
- 存储桶配置 buckets 的动态加载机制,以及通过 RUSTFS_BUCKET_TEXTURES 和 RUSTFS_BUCKET_AVATARS 环境变量为不同用途(材质与头像)设置存储桶
- 基于 RustFSConfig 结构体的安全配置最佳实践,包括凭证管理与 SSL 配置建议
## 项目结构
CarrotSkin 的对象存储配置由“配置层 → 存储层 → 业务层”三层协作完成:
- 配置层:负责从环境变量加载并合并 RustFSConfig支持运行时覆盖
- 存储层:封装 S3 兼容客户端minio-go提供桶名映射与预签名上传能力
- 业务层:根据文件类型生成上传 URL按需选择对应存储桶
```mermaid
graph TB
subgraph "配置层"
CFG["RustFSConfig<br/>endpoint/access_key/secret_key/use_ssl/buckets"]
CMGR["配置管理器<br/>Init/GetRustFSConfig"]
end
subgraph "存储层"
SMGR["存储管理器<br/>Init/GetClient"]
SCLI["StorageClient<br/>minio.Client + buckets 映射"]
end
subgraph "业务层"
SVC["上传服务<br/>生成头像/材质上传URL"]
end
CMGR --> CFG
SMGR --> SCLI
SVC --> SCLI
CFG --> SMGR
SCLI --> SVC
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
- [cmd/server/main.go](file://cmd/server/main.go#L63-L69)
## 核心组件
- RustFSConfig定义对象存储的端点、凭证、协议开关与桶映射
- 配置管理器:提供全局单例的配置加载与获取能力
- 存储管理器:提供全局单例的存储客户端初始化与获取能力
- StorageClient封装 minio-go 客户端,提供桶名映射与预签名上传能力
- 上传服务:按文件类型生成预签名上传 URL并选择对应存储桶
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
## 架构总览
下图展示从应用启动到生成上传 URL 的关键流程,以及各组件之间的依赖关系。
```mermaid
sequenceDiagram
participant Main as "主程序"
participant CfgMgr as "配置管理器"
participant Cfg as "RustFSConfig"
participant StorMgr as "存储管理器"
participant StorCli as "StorageClient"
participant Svc as "上传服务"
Main->>CfgMgr : 调用 Init()
CfgMgr->>CfgMgr : 加载环境变量并合并默认值
CfgMgr-->>Main : 返回全局 RustFSConfig
Main->>StorMgr : 调用 Init(RustFSConfig)
StorMgr->>StorCli : NewStorage(cfg)
StorCli-->>StorMgr : 返回 StorageClient
StorMgr-->>Main : 初始化完成
Svc->>StorCli : GetBucket("avatars"/"textures")
Svc->>StorCli : GeneratePresignedPostURL(...)
StorCli-->>Svc : 返回 PostURL + FormData + FileURL
```
图表来源
- [cmd/server/main.go](file://cmd/server/main.go#L27-L70)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
## 详细组件分析
### RustFSConfig 结构体与配置项说明
- endpointRustFS 服务地址(含端口)。用于构建预签名上传 URL 的最终访问地址与协议选择
- access_key / secret_key访问凭证用于构造静态凭据并建立 S3 兼容客户端
- use_ssl是否启用 HTTPS 协议;影响预签名上传 URL 的协议与文件访问 URL 的协议
- buckets存储桶映射键为用途标识如 "textures"、"avatars"),值为实际桶名
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
### 配置加载与动态覆盖
- 默认值:通过 viper 设置默认值,包含 endpoint 与 use_ssl 的默认值
- 环境变量映射:将 CARROTSKIN 前缀的环境变量绑定到配置键
- 动态覆盖overrideFromEnv 支持通过 RUSTFS_BUCKET_TEXTURES 与 RUSTFS_BUCKET_AVATARS 动态注入存储桶映射,无需修改配置文件
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
### 存储客户端初始化与连接测试
- 客户端创建:基于 endpoint、access_key、secret_key、use_ssl 构造 minio-go 客户端
- 连接测试:当凭证非空时,尝试列举桶以验证连通性
- 桶映射:将 RustFSConfig 中的 buckets 直接注入 StorageClient
章节来源
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L49)
### 存储桶映射与上传 URL 生成
- GetBucket按用途键"textures" 或 "avatars")从映射中取桶名
- 预签名上传:根据文件类型与大小范围生成 POST 策略,返回 PostURL、FormData 与最终 FileURL
- 协议选择:根据 use_ssl 决定协议http/https
章节来源
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L57-L121)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
### 业务层调用链(头像上传)
```mermaid
sequenceDiagram
participant Svc as "上传服务"
participant StorCli as "StorageClient"
participant Cfg as "RustFSConfig"
Svc->>Svc : 校验文件名与扩展名
Svc->>StorCli : GetBucket("avatars")
StorCli-->>Svc : 返回桶名
Svc->>StorCli : GeneratePresignedPostURL(bucket, objectName, min/max/expire, useSSL, endpoint)
StorCli-->>Svc : 返回 PostURL + FormData + FileURL
Svc-->>Svc : 组装响应并返回
```
图表来源
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
### 业务层调用链(材质上传)
```mermaid
sequenceDiagram
participant Svc as "上传服务"
participant StorCli as "StorageClient"
participant Cfg as "RustFSConfig"
Svc->>Svc : 校验文件名与扩展名
Svc->>Svc : 校验材质类型(SKIN/CAPE)
Svc->>StorCli : GetBucket("textures")
StorCli-->>Svc : 返回桶名
Svc->>StorCli : GeneratePresignedPostURL(...)
StorCli-->>Svc : 返回 PostURL + FormData + FileURL
Svc-->>Svc : 组装响应并返回
```
图表来源
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
### 存储桶动态加载机制
- 环境变量注入RUSTFS_BUCKET_TEXTURES 与 RUSTFS_BUCKET_AVATARS 分别为“材质”和“头像”用途注入桶名
- 映射合并overrideFromEnv 在 RustFS.Buckets 为空时创建映射,再写入对应键值
- 无配置文件依赖:完全通过环境变量驱动,便于容器化部署与多环境切换
```mermaid
flowchart TD
Start(["启动"]) --> LoadEnv["加载环境变量"]
LoadEnv --> Override["overrideFromEnv 动态覆盖"]
Override --> CheckTextures{"RUSTFS_BUCKET_TEXTURES 是否存在"}
CheckTextures --> |是| PutTextures["写入 buckets['textures']"]
CheckTextures --> |否| Next1["跳过"]
PutTextures --> Next2["继续检查"]
Next1 --> Next2
Next2 --> CheckAvatars{"RUSTFS_BUCKET_AVATARS 是否存在"}
CheckAvatars --> |是| PutAvatars["写入 buckets['avatars']"]
CheckAvatars --> |否| End(["结束"])
PutAvatars --> End
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
## 依赖关系分析
- 配置层依赖 viper 与 godotenv负责环境变量加载与默认值设置
- 存储层依赖 minio-go封装客户端与桶映射
- 业务层依赖存储层提供的预签名上传能力
- 启动流程在 main 中依次初始化配置、日志、数据库、JWT、Redis、对象存储与邮件服务
```mermaid
graph LR
Viper["viper/godotenv"] --> Config["RustFSConfig"]
Config --> Manager["配置管理器"]
Manager --> StorageMgr["存储管理器"]
StorageMgr --> StorageClient["StorageClient(minio-go)"]
StorageClient --> UploadSvc["上传服务"]
Main["main"] --> Manager
Main --> StorageMgr
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
- [cmd/server/main.go](file://cmd/server/main.go#L27-L70)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
- [cmd/server/main.go](file://cmd/server/main.go#L27-L70)
## 性能考量
- 预签名上传避免了服务端中转,降低带宽与延迟
- 上传 URL 过期时间短(默认 15 分钟),减少凭证暴露风险
- 桶映射为内存查找,开销极低
- 连接测试仅在凭证非空时进行,避免不必要的网络往返
[本节为通用指导,不涉及具体文件分析]
## 故障排查指南
- 配置未初始化
- 现象:调用 GetRustFSConfig/MustGetRustFSConfig 或 GetClient/MustGetClient 报错
- 排查:确认已在 main 中调用 config.Init() 与 storage.Init()
- 凭证为空导致连接测试跳过
- 现象NewStorage 成功但未进行 ListBuckets 测试
- 排查:若需验证连通性,请提供 access_key 与 secret_key
- 存储桶不存在
- 现象GetBucket 返回错误
- 排查:确认 RUSTFS_BUCKET_TEXTURES/RUSTFS_BUCKET_AVATARS 已正确设置,且与 RustFS 实际桶名一致
- 预签名上传失败
- 现象GeneratePresignedPostURL 返回错误
- 排查:检查 min/max 大小范围、过期时间、use_ssl 与 endpoint 是否匹配
章节来源
- [pkg/config/manager.go](file://pkg/config/manager.go#L31-L63)
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L29-L44)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L49)
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L57-L121)
## 结论
- CarrotSkin 通过 RustFSConfig 将对象存储配置集中管理,并以环境变量驱动,实现灵活部署
- 通过 RUSTFS_BUCKET_TEXTURES 与 RUSTFS_BUCKET_AVATARS 实现“材质/头像”两类资源的桶级隔离
- 基于 minio-go 的预签名上传机制,既保证安全性又提升性能
- 建议遵循安全最佳实践最小权限、短期凭证、HTTPS以保障生产安全
[本节为总结性内容,不涉及具体文件分析]
## 附录
### 环境变量与默认值对照
- RUSTFS_ENDPOINTRustFS 服务地址,默认值由 viper 设置
- RUSTFS_ACCESS_KEY访问密钥
- RUSTFS_SECRET_KEY私有密钥
- RUSTFS_USE_SSL是否启用 HTTPS
- RUSTFS_BUCKET_TEXTURES材质用途桶名
- RUSTFS_BUCKET_AVATARS头像用途桶名
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
- [start.sh](file://start.sh#L22-L27)
### 启动脚本示例
- start.sh 展示了如何设置对象存储相关环境变量,便于本地开发与测试
章节来源
- [start.sh](file://start.sh#L22-L27)

View File

@@ -1,364 +0,0 @@
# 数据库配置
<cite>
**本文引用的文件**
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/database/postgres.go](file://pkg/database/postgres.go)
- [pkg/database/manager.go](file://pkg/database/manager.go)
- [cmd/server/main.go](file://cmd/server/main.go)
- [start.sh](file://start.sh)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件聚焦 CarrotSkin 项目的数据库连接与连接池配置,系统性说明以下内容:
- 连接参数 database.driver、host、port、username、password、database、ssl_mode、timezone 的用途与默认值来源
- 连接池参数 max_idle_conns、max_open_conns、conn_max_lifetime 的工作机制与性能影响
- 如何通过 DATABASE_* 环境变量覆盖默认配置
- 结合 DatabaseConfig 结构体与数据库初始化流程的最佳实践,包含生产环境安全建议与连接池调优策略
## 项目结构
数据库相关能力由配置层与数据库层协同实现:
- 配置层负责加载默认值、绑定环境变量映射,并支持从环境变量覆盖连接池参数
- 数据库层负责根据配置构建 DSN、初始化 GORM 连接、设置连接池并进行连通性测试
- 应用入口在启动时加载配置、初始化数据库并执行迁移
```mermaid
graph TB
subgraph "配置层"
CFG["pkg/config/config.go<br/>DatabaseConfig/默认值/环境映射"]
end
subgraph "数据库层"
DBM["pkg/database/manager.go<br/>Init/GetDB/AutoMigrate/Close"]
PG["pkg/database/postgres.go<br/>New/GetDSN"]
end
subgraph "应用入口"
MAIN["cmd/server/main.go<br/>加载配置/初始化数据库/迁移"]
end
ENV["环境变量<br/>DATABASE_*"]
ENV --> CFG
CFG --> DBM
DBM --> PG
MAIN --> CFG
MAIN --> DBM
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
## 核心组件
- DatabaseConfig 结构体:承载数据库连接与连接池配置
- 配置加载与覆盖:默认值、环境变量映射、连接池参数覆盖
- 数据库初始化:构建 DSN、初始化 GORM、设置连接池、Ping 测试
- 应用启动流程:加载配置 → 初始化数据库 → 自动迁移
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
## 架构总览
下图展示从配置到数据库连接的关键调用链路与职责分工。
```mermaid
sequenceDiagram
participant Entrypoint as "应用入口(main.go)"
participant Cfg as "配置(Config.Load)"
participant DBMgr as "数据库管理器(database.Init)"
participant DB as "数据库(New)"
participant SQL as "底层sql.DB"
Entrypoint->>Cfg : "加载配置"
Cfg-->>Entrypoint : "返回完整配置"
Entrypoint->>DBMgr : "Init(DatabaseConfig)"
DBMgr->>DB : "New(DatabaseConfig)"
DB->>DB : "构造DSN"
DB->>SQL : "gorm.Open + 获取*sql.DB"
DB->>SQL : "SetMaxIdleConns/SetMaxOpenConns/SetConnMaxLifetime"
DB->>SQL : "Ping() 测试连接"
DB-->>DBMgr : "*gorm.DB"
DBMgr-->>Entrypoint : "初始化完成"
```
图表来源
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L33)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
## 详细组件分析
### DatabaseConfig 字段与用途
- database.driver
- 作用:驱动类型标识,用于控制日志级别与后续扩展点
- 默认值:来自默认配置
- 环境变量映射DATABASE_DRIVER
- database.host
- 作用:数据库主机地址
- 默认值:来自默认配置
- 环境变量映射DATABASE_HOST
- database.port
- 作用:数据库端口
- 默认值:来自默认配置
- 环境变量映射DATABASE_PORT
- database.username
- 作用:数据库用户名
- 默认值:来自默认配置
- 环境变量映射DATABASE_USERNAME
- database.password
- 作用:数据库密码
- 默认值:来自默认配置
- 环境变量映射DATABASE_PASSWORD
- database.database
- 作用:数据库名
- 默认值:来自默认配置
- 环境变量映射DATABASE_NAME
- database.ssl_mode
- 作用SSL 模式(如 disable/require 等),影响 TLS 连接
- 默认值:来自默认配置
- 环境变量映射DATABASE_SSL_MODE
- database.timezone
- 作用:时区设置,影响时间字段的时区行为
- 默认值:来自默认配置
- 环境变量映射DATABASE_TIMEZONE
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
### 连接池配置项与工作机制
- max_idle_conns
- 作用:最大空闲连接数,控制连接池中空闲连接上限
- 默认值:来自默认配置
- 环境变量覆盖DATABASE_MAX_IDLE_CONNS
- max_open_conns
- 作用:最大打开连接数,限制并发连接上限
- 默认值:来自默认配置
- 环境变量覆盖DATABASE_MAX_OPEN_CONNS
- conn_max_lifetime
- 作用:连接最大存活时间,到期后连接会被回收
- 默认值:来自默认配置
- 环境变量覆盖DATABASE_CONN_MAX_LIFETIME支持时长字符串如“1h”
这些参数在 New 中通过底层 sql.DB 设置,并在 Ping 成功后生效。
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L49-L53)
### 环境变量覆盖与默认值
- 默认值来源
- 通过 viper.SetDefault 设置,确保即使未提供对应环境变量也能有合理默认
- 环境变量映射
- 使用 viper.BindEnv 将配置键映射到 DATABASE_* 前缀的环境变量
- 连接池参数覆盖
- 通过 overrideFromEnv 从环境变量读取整型或时长字符串并覆盖默认值
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
### 数据库初始化流程与 DSN 构造
- DSN 构造
- New 中依据 DatabaseConfig 组装 DSN包含 host、port、user、password、dbname、sslmode、TimeZone
- GORM 初始化
- 根据 driver 决定日志级别;禁用迁移时的外键约束以避免循环依赖
- 连接池设置
- SetMaxIdleConns、SetMaxOpenConns、SetConnMaxLifetime
- 连通性测试
- Ping 成功后返回 gorm.DB 实例
章节来源
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
### 应用启动中的数据库初始化
- 配置加载Load 会加载 .env 并解析为 Config
- 数据库初始化Init 接收 DatabaseConfig内部调用 New 完成连接与池设置
- 自动迁移AutoMigrate 按顺序迁移模型,确保外键依赖关系正确
- 资源清理Close 关闭底层 sql.DB
章节来源
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
- [pkg/database/manager.go](file://pkg/database/manager.go#L101-L114)
## 依赖关系分析
```mermaid
classDiagram
class Config {
+ServerConfig Server
+DatabaseConfig Database
+RedisConfig Redis
+RustFSConfig RustFS
+JWTConfig JWT
+CasbinConfig Casbin
+LogConfig Log
+UploadConfig Upload
+EmailConfig Email
}
class DatabaseConfig {
+string Driver
+string Host
+int Port
+string Username
+string Password
+string Database
+string SSLMode
+string Timezone
+int MaxIdleConns
+int MaxOpenConns
+duration ConnMaxLifetime
}
class Manager {
+Init(cfg, logger) error
+GetDB() (*gorm.DB, error)
+MustGetDB() *gorm.DB
+AutoMigrate(logger) error
+Close() error
}
class Postgres {
+New(cfg) (*gorm.DB, error)
+GetDSN(cfg) string
}
Config --> DatabaseConfig : "包含"
Manager --> DatabaseConfig : "使用"
Postgres --> DatabaseConfig : "使用"
Manager --> Postgres : "调用"
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L13-L24)
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
## 性能考量
- 连接池参数建议
- max_idle_conns建议与 max_open_conns 保持一定比例,避免过多空闲连接占用资源
- max_open_conns应结合数据库最大连接数与应用并发量综合评估避免超过数据库限制导致连接排队
- conn_max_lifetime建议设置为小于数据库连接超时阈值确保连接健康与复用效率
- 日志级别
- 当 driver 为 postgres 时GORM 日志级别提升为 Info便于调试在生产环境可考虑降低日志级别以减少开销
- 迁移策略
- AutoMigrate 顺序需保证被引用表先于引用表创建,避免外键约束冲突
章节来源
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L25-L32)
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
## 故障排查指南
- 未初始化即使用数据库
- 现象:调用 GetDB 返回错误提示“数据库未初始化”
- 处理:确保在应用启动阶段先调用 database.Init
- 连接失败
- 检查 DSN 参数是否正确host/port/username/password/database/ssl_mode/timezone
- 确认数据库可达与凭据有效
- 连接池异常
- 检查 max_idle_conns、max_open_conns 是否设置过小或过大
- 检查 conn_max_lifetime 是否导致频繁重建连接
- 环境变量未生效
- 确认环境变量前缀与键名匹配DATABASE_*
- 确认 overrideFromEnv 可正确解析整型与 duration 类型
章节来源
- [pkg/database/manager.go](file://pkg/database/manager.go#L35-L41)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
## 结论
- CarrotSkin 的数据库配置采用“默认值 + 环境变量覆盖”的设计,既保证开箱即用,又允许灵活定制
- 连接池参数通过 DATABASE_MAX_IDLE_CONNS、DATABASE_MAX_OPEN_CONNS、DATABASE_CONN_MAX_LIFETIME 进行覆盖,建议结合业务并发与数据库能力进行调优
- 生产环境建议启用 SSLssl_mode、严格凭据管理与最小权限原则并适当降低日志级别以优化性能
## 附录
### 环境变量与默认值对照表
- database.driver
- 环境变量DATABASE_DRIVER
- 默认值postgres
- database.host
- 环境变量DATABASE_HOST
- 默认值localhost
- database.port
- 环境变量DATABASE_PORT
- 默认值5432
- database.username
- 环境变量DATABASE_USERNAME
- 默认值:空字符串
- database.password
- 环境变量DATABASE_PASSWORD
- 默认值:空字符串
- database.database
- 环境变量DATABASE_NAME
- 默认值:空字符串
- database.ssl_mode
- 环境变量DATABASE_SSL_MODE
- 默认值disable
- database.timezone
- 环境变量DATABASE_TIMEZONE
- 默认值Asia/Shanghai
- database.max_idle_conns
- 环境变量DATABASE_MAX_IDLE_CONNS
- 默认值10
- database.max_open_conns
- 环境变量DATABASE_MAX_OPEN_CONNS
- 默认值100
- database.conn_max_lifetime
- 环境变量DATABASE_CONN_MAX_LIFETIME
- 默认值1h
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
### 生产环境安全与连接池调优建议
- 安全建议
- 将 ssl_mode 设为 require 或 verify-full视数据库支持而定并配合证书校验
- 使用专用数据库用户与最小权限策略,定期轮换密码
- 通过环境变量注入敏感信息,避免硬编码
- 连接池调优
- 初步以 max_idle_conns ≈ 1/10 max_open_conns 开始,结合压测结果调整
- 将 conn_max_lifetime 设置为略小于数据库连接超时阈值,平衡资源回收与连接复用
- 监控数据库连接数与等待队列,动态调整 max_open_conns
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L49-L53)
### 启动脚本中的数据库环境变量示例
- 示例脚本展示了如何设置 DATABASE_HOST、DATABASE_PORT、DATABASE_USERNAME、DATABASE_PASSWORD、DATABASE_NAME、DATABASE_SSL_MODE、DATABASE_TIMEZONE 等环境变量
章节来源
- [start.sh](file://start.sh#L1-L41)

View File

@@ -1,367 +0,0 @@
# 日志配置
<cite>
**本文引用的文件**
- [cmd/server/main.go](file://cmd/server/main.go)
- [pkg/config/config.go](file://pkg/config/config.go)
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
- [pkg/logger/manager.go](file://pkg/logger/manager.go)
- [internal/middleware/logger.go](file://internal/middleware/logger.go)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go)
- [go.mod](file://go.mod)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向 CarrotSkin 项目的开发者与运维人员系统性说明日志系统的配置与使用方式。重点围绕以下配置项展开log.level、format、output、max_size、max_backups、max_age 和 compress并结合 LogConfig 结构体与实际代码实现给出不同日志级别选择策略、JSON 格式与文本格式的适用场景、以及基于大小与天数的日志轮转工作机制。最后提供生产环境监控集成建议与最佳实践。
## 项目结构
日志系统由“配置加载层”“日志工厂层”“全局管理器层”“中间件与业务使用层”四部分组成:
- 配置加载层:从环境变量加载并解析为 Config其中包含 LogConfig 字段。
- 日志工厂层:根据 LogConfig 构建 zap.Logger。
- 全局管理器层:提供 Init、GetLogger、MustGetLogger 等接口,确保单例与线程安全。
- 中间件与业务使用层:通过中间件注入日志实例,统一记录 HTTP 请求与异常恢复。
```mermaid
graph TB
A["配置加载<br/>pkg/config/config.go"] --> B["日志工厂<br/>pkg/logger/logger.go"]
B --> C["全局管理器<br/>pkg/logger/manager.go"]
C --> D["中间件使用<br/>internal/middleware/logger.go"]
C --> E["恢复中间件<br/>internal/middleware/recovery.go"]
C --> F["应用入口初始化<br/>cmd/server/main.go"]
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
## 核心组件
- LogConfig 结构体:定义日志系统的核心配置项,来源于环境变量映射。
- New根据 LogConfig 构建 zap.Logger支持 JSON/Console 编码器、stdout 或文件输出。
- Init/GetLogger/MustGetLogger全局单例管理保证线程安全与幂等初始化。
- 中间件 Logger/Recovery在请求处理前后记录日志并在 panic 时记录堆栈与上下文。
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
## 架构总览
下图展示了从配置到日志输出的关键流程:应用启动时加载配置,初始化日志管理器,随后中间件与业务代码通过全局日志实例进行记录。
```mermaid
sequenceDiagram
participant App as "应用入口<br/>cmd/server/main.go"
participant Cfg as "配置加载<br/>pkg/config/config.go"
participant LMgr as "日志管理器<br/>pkg/logger/manager.go"
participant LNew as "日志工厂<br/>pkg/logger/logger.go"
participant MW as "中间件<br/>internal/middleware/logger.go"
participant Rec as "恢复中间件<br/>internal/middleware/recovery.go"
App->>Cfg : 加载配置
Cfg-->>App : 返回 Config(LogConfig)
App->>LMgr : Init(LogConfig)
LMgr->>LNew : New(LogConfig)
LNew-->>LMgr : 返回 zap.Logger
LMgr-->>App : 初始化完成
App->>MW : 注入日志实例
App->>Rec : 注入日志实例
MW-->>App : 记录请求日志
Rec-->>App : 记录 panic 与堆栈
```
图表来源
- [cmd/server/main.go](file://cmd/server/main.go#L27-L40)
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L20-L29)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L14-L68)
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L10-L39)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L11-L29)
## 详细组件分析
### 配置项详解与选择策略
- log.level
- 取值范围debug、info、warn、error默认 info。
- 选择策略:
- 开发/调试debug便于定位问题。
- 生产info平衡可观测性与性能仅在需要深入排查时临时提升到 debug。
- 严重问题error聚焦错误事件避免过多噪声。
- log.format
- 取值console、json。
- 适用场景:
- console本地开发终端阅读友好便于快速定位。
- json便于日志收集与结构化分析适合生产与集中化监控。
- log.output
- 取值:空/“stdout”表示输出到标准输出其他路径将自动创建目录并写入文件。
- 适用场景:
- 容器化部署stdout配合容器日志采集。
- 传统部署:文件路径,结合日志轮转策略。
- log.max_size、log.max_backups、log.max_age、log.compress
- 作用:控制日志轮转策略(按大小与天数)与压缩开关。
- 工作机制(基于大小与天数):
- 当单个日志文件达到 max_sizeMB时触发轮转。
- 最多保留 max_backups 个历史备份文件。
- 历史文件保留不超过 max_age
- compress=true 时对旧文件进行压缩,节省磁盘空间。
- 注意:当前代码实现中并未直接使用上述字段进行轮转配置,而是采用文件输出与基础写入同步。若需启用基于大小/天数的轮转,可在日志工厂中引入外部轮转库(如 lumberjack并传入上述参数。
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
- [pkg/config/config.go](file://pkg/config/config.go#L170-L178)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L43-L60)
### 日志级别选择策略
- debug用于开发阶段的详细追踪包含大量上下文信息。
- info生产环境默认级别记录关键业务事件与系统状态。
- warn潜在问题或异常流程需关注但不影响整体运行。
- error错误事件必须处理与上报。
章节来源
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L16-L28)
### JSON 格式与文本格式的适用场景
- JSON
- 优点:结构化强,便于日志聚合、检索与告警。
- 适用:生产环境、集中化日志平台(如 ELK、Loki、Cloud Logging
- Console
- 优点:人类可读性强,适合本地开发与快速排障。
- 适用:本地调试、临时诊断。
章节来源
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L30-L41)
### 日志轮转策略(基于大小与天数)
- 当前实现要点:
- 输出到文件时会自动创建目录并追加写入。
- 未显式配置基于大小/天数的轮转。
- 建议扩展方案:
- 引入外部轮转库(如 lumberjack在 New 中根据 max_size、max_backups、max_age、compress 进行轮转配置。
- 将轮转配置作为可选参数传入,保持现有接口兼容。
章节来源
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L43-L60)
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
### 日志工厂与全局管理器
- New根据 LogConfig 构建 zap.Logger设置级别、编码器与输出目标。
- Init/GetLogger/MustGetLogger提供线程安全的单例初始化与获取能力避免重复初始化。
```mermaid
classDiagram
class LogConfig {
+string level
+string format
+string output
+int max_size
+int max_backups
+int max_age
+bool compress
}
class LoggerFactory {
+New(cfg LogConfig) *zap.Logger
}
class LogManager {
+Init(cfg LogConfig) error
+GetLogger() (*zap.Logger, error)
+MustGetLogger() *zap.Logger
}
LogConfig --> LoggerFactory : "输入"
LoggerFactory --> LogManager : "被调用"
```
图表来源
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L14-L68)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L20-L46)
章节来源
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L14-L68)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
### 中间件与业务使用
- Logger 中间件:记录每次 HTTP 请求的方法、路径、状态码、耗时、客户端 IP、User-Agent 等。
- Recovery 中间件:捕获 panic 并记录错误、路径、方法、IP 与完整堆栈,返回统一错误响应。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Router as "Gin 路由"
participant MW as "日志中间件"
participant Handler as "业务处理器"
participant Rec as "恢复中间件"
Client->>Router : 发起请求
Router->>MW : 进入日志中间件
MW->>MW : 记录请求开始时间与上下文
MW->>Handler : 继续处理
Handler-->>MW : 返回响应
MW->>MW : 记录状态码、耗时、IP、UA
MW-->>Router : 返回响应
Note over MW : 若发生 panic由恢复中间件接管
Router->>Rec : 进入恢复中间件
Rec->>Rec : 记录错误与堆栈
Rec-->>Client : 返回 500 错误
```
图表来源
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L10-L39)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L11-L29)
- [cmd/server/main.go](file://cmd/server/main.go#L81-L90)
章节来源
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
- [cmd/server/main.go](file://cmd/server/main.go#L81-L90)
## 依赖关系分析
- 外部依赖zap 用于高性能日志记录viper 用于配置加载godotenv 用于 .env 支持。
- 内部依赖:配置模块提供 LogConfig日志模块提供 Init/GetLogger中间件依赖日志实例。
```mermaid
graph LR
subgraph "外部依赖"
Z["go.uber.org/zap"]
V["github.com/spf13/viper"]
D["github.com/joho/godotenv"]
end
subgraph "内部模块"
CFG["pkg/config/config.go"]
LOGF["pkg/logger/logger.go"]
LOGM["pkg/logger/manager.go"]
MWL["internal/middleware/logger.go"]
MWR["internal/middleware/recovery.go"]
MAIN["cmd/server/main.go"]
end
V --> CFG
D --> CFG
CFG --> LOGM
LOGM --> LOGF
MAIN --> LOGM
MAIN --> MWL
MAIN --> MWR
Z --> LOGF
Z --> MWL
Z --> MWR
```
图表来源
- [go.mod](file://go.mod#L7-L22)
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
章节来源
- [go.mod](file://go.mod#L7-L22)
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
## 性能考量
- 使用 zap具备高性能与零分配特性适合高并发场景。
- 选择合适的日志级别:生产环境默认 info避免 debug 的额外开销。
- 输出目标选择stdout 便于容器日志采集;文件输出需注意磁盘 IO 与轮转策略。
- 压缩与保留策略:合理设置 max_size、max_backups、max_age 与 compress平衡磁盘占用与检索效率。
## 故障排查指南
- 未初始化日志即使用:
- 现象:调用 GetLogger 返回错误提示“日志未初始化,请先调用 logger.Init()”。
- 排查:确认应用入口是否在启动时调用了 logger.Init(cfg.Log)。
- 输出到文件失败:
- 现象:初始化日志时报错,无法打开文件。
- 排查:检查 log.output 指定路径是否存在写权限,目录是否可创建。
- panic 未记录:
- 现象:服务崩溃但无日志记录。
- 排查:确认已注册 Recovery 中间件并注入了日志实例。
- 日志轮转未生效:
- 现象:日志文件持续增大。
- 排查:当前实现未使用 max_size/max_backups/max_age/compress 进行轮转,需扩展轮转逻辑。
章节来源
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L31-L37)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L43-L60)
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L11-L29)
- [cmd/server/main.go](file://cmd/server/main.go#L81-L90)
## 结论
- CarrotSkin 的日志系统以 zap 为核心,通过配置驱动实现灵活的日志级别、编码器与输出目标切换。
- 当前未直接启用基于大小/天数的轮转,建议在日志工厂中引入轮转库并使用 LogConfig 的 max_size、max_backups、max_age、compress 字段。
- 在生产环境中推荐使用 JSON 格式与 stdout 输出,结合集中化日志平台与告警策略,提升可观测性与可维护性。
## 附录
### 配置项一览与默认值
- log.level默认 info
- log.format默认 json
- log.output默认 logs/app.log文件输出
- log.max_size默认 100 MB
- log.max_backups默认 3
- log.max_age默认 28 天
- log.compress默认 true
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L170-L178)
### 环境变量映射
- LOG_LEVEL → log.level
- LOG_FORMAT → log.format
- LOG_OUTPUT → log.output
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L224-L228)
### 配置示例与最佳实践
- 示例一:本地开发(控制台输出,便于阅读)
- LOG_LEVEL=debug
- LOG_FORMAT=console
- LOG_OUTPUT=stdout
- 示例二:生产(结构化日志,集中化采集)
- LOG_LEVEL=info
- LOG_FORMAT=json
- LOG_OUTPUT=stdout
- 示例三:传统服务器(文件输出,配合轮转)
- LOG_LEVEL=info
- LOG_FORMAT=json
- LOG_OUTPUT=/var/log/carrotskin/app.log
- max_size=100
- max_backups=3
- max_age=28
- compress=true
- 最佳实践:
- 生产环境统一使用 JSON 格式与 stdout便于容器日志采集。
- 严格控制日志级别,避免 debug 在生产长期开启。
- 对关键路径增加结构化字段(如用户 ID、请求 ID便于关联追踪。
- 在中间件中统一记录请求上下文,确保异常时具备足够信息。
- 如需文件轮转,建议在日志工厂中引入轮转库并使用上述配置项。
章节来源
- [pkg/config/config.go](file://pkg/config/config.go#L170-L178)
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L30-L41)
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L30-L37)

View File

@@ -1,205 +0,0 @@
# 服务器配置
<cite>
**本文档引用文件**
- [config.go](file://pkg/config/config.go)
- [manager.go](file://pkg/config/manager.go)
- [main.go](file://cmd/server/main.go)
- [start.sh](file://start.sh)
- [dev.sh](file://scripts/dev.sh)
</cite>
## 目录
1. [简介](#简介)
2. [服务器配置项详解](#服务器配置项详解)
3. [环境变量覆盖机制](#环境变量覆盖机制)
4. [配置示例](#配置示例)
5. [性能调优建议](#性能调优建议)
6. [配置结构体说明](#配置结构体说明)
## 简介
CarrotSkin项目采用基于环境变量的配置管理机制通过Viper库实现灵活的配置加载与覆盖。服务器模块的配置主要集中在`ServerConfig`结构体中,定义了端口、运行模式、读写超时等关键参数。本文档详细说明这些配置项的作用、默认值以及如何通过环境变量进行覆盖,为开发者提供清晰的配置指导。
**Section sources**
- [config.go](file://pkg/config/config.go#L26-L32)
- [main.go](file://cmd/server/main.go#L27-L124)
## 服务器配置项详解
### server.port
- **作用**:指定服务器监听的端口号
- **数据类型**string
- **默认值**":8080"
- **说明**:该配置值应包含冒号前缀,表示监听所有网络接口上的指定端口。例如":8080"表示监听8080端口。
### server.mode
- **作用**:设置服务器运行模式
- **数据类型**string
- **默认值**"debug"
- **说明**:支持"debug"和"production"两种模式。在生产模式下Gin框架会自动切换到发布模式关闭调试信息输出提高性能和安全性。
### server.read_timeout
- **作用**设置HTTP服务器读取请求的超时时间
- **数据类型**time.Duration
- **默认值**"30s"
- **说明**:防止客户端长时间不发送数据导致服务器资源被占用。超时后连接将被关闭。
### server.write_timeout
- **作用**设置HTTP服务器写入响应的超时时间
- **数据类型**time.Duration
- **默认值**"30s"
- **说明**:防止响应过程过长占用服务器资源。对于大文件下载等长时间操作,可能需要适当增加此值。
**Section sources**
- [config.go](file://pkg/config/config.go#L137-L141)
- [config.go](file://pkg/config/config.go#L26-L32)
## 环境变量覆盖机制
CarrotSkin项目支持通过环境变量覆盖默认配置实现不同环境下的灵活配置管理。环境变量前缀为`CARROTSKIN`,具体映射关系如下:
| 配置项 | 环境变量 | 优先级 |
|--------|----------|--------|
| server.port | CARROTSKIN_SERVER_PORT | 高 |
| server.mode | CARROTSKIN_SERVER_MODE | 高 |
| server.read_timeout | CARROTSKIN_SERVER_READ_TIMEOUT | 高 |
| server.write_timeout | CARROTSKIN_SERVER_WRITE_TIMEOUT | 高 |
配置加载流程:
1. 加载`.env`文件中的环境变量
2. 设置各项配置的默认值
3. 绑定环境变量映射关系
4. 从环境变量中读取并覆盖配置
```mermaid
flowchart TD
A[开始] --> B[加载.env文件]
B --> C[设置默认配置值]
C --> D[绑定环境变量映射]
D --> E[从环境变量覆盖配置]
E --> F[返回配置实例]
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L108-L134)
**Section sources**
- [config.go](file://pkg/config/config.go#L192-L196)
- [config.go](file://pkg/config/config.go#L108-L134)
## 配置示例
### 开发环境配置
在开发环境中,通常使用默认配置或通过`dev.sh`脚本启动:
```bash
# 启动开发服务器
./scripts/dev.sh
```
或者手动设置环境变量:
```bash
export CARROTSKIN_SERVER_PORT=":8080"
export CARROTSKIN_SERVER_MODE="debug"
export CARROTSKIN_SERVER_READ_TIMEOUT="30s"
export CARROTSKIN_SERVER_WRITE_TIMEOUT="30s"
go run ./cmd/server
```
### 生产环境配置
生产环境建议使用`start.sh`脚本,并设置更严格的配置:
```bash
# 生产环境启动脚本示例
export CARROTSKIN_SERVER_PORT=":80"
export CARROTSKIN_SERVER_MODE="production"
export CARROTSKIN_SERVER_READ_TIMEOUT="15s"
export CARROTSKIN_SERVER_WRITE_TIMEOUT="15s"
./start.sh
```
### Docker环境配置
在Docker环境中可以通过环境变量进行配置
```dockerfile
environment:
- CARROTSKIN_SERVER_PORT=:80
- CARROTSKIN_SERVER_MODE=production
- CARROTSKIN_SERVER_READ_TIMEOUT=20s
- CARROTSKIN_SERVER_WRITE_TIMEOUT=20s
```
**Section sources**
- [start.sh](file://start.sh#L1-L37)
- [dev.sh](file://scripts/dev.sh#L1-L29)
## 性能调优建议
### 高并发场景下的超时设置
在高并发场景下,合理的超时设置对系统稳定性至关重要:
- **低延迟场景**对于API服务等需要快速响应的场景建议将读写超时设置为10-15秒
- **大文件传输场景**对于包含文件上传下载的功能建议适当增加write_timeout至60秒以上
- **内网服务**如果是内网服务且网络环境稳定可以将超时时间设置得更短如5-10秒
### 连接池与超时的协同优化
```mermaid
flowchart LR
A[客户端请求] --> B{连接建立}
B --> |成功| C[读取请求]
C --> D{读取超时}
D --> |超时| E[关闭连接]
D --> |成功| F[处理请求]
F --> G[写入响应]
G --> H{写入超时}
H --> |超时| I[关闭连接]
H --> |成功| J[完成响应]
```
**Diagram sources**
- [main.go](file://cmd/server/main.go#L93-L98)
**Section sources**
- [main.go](file://cmd/server/main.go#L93-L98)
- [config.go](file://pkg/config/config.go#L139-L140)
## 配置结构体说明
### ServerConfig 结构体
```go
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
}
```
- **Port**:端口号,字符串类型,需包含冒号前缀
- **Mode**:运行模式,可选值为"debug"或"production"
- **ReadTimeout**读取超时time.Duration类型支持"30s"、"1m"等格式
- **WriteTimeout**写入超时time.Duration类型支持"30s"、"1m"等格式
### 配置初始化流程
```mermaid
sequenceDiagram
participant Main as main.go
participant Config as config.go
participant Viper as Viper库
Main->>Config : config.Init()
Config->>Viper : 加载.env文件
Viper-->>Config : 返回环境变量
Config->>Config : setDefaults()
Config->>Config : setupEnvMappings()
Config->>Config : overrideFromEnv()
Config-->>Main : 返回配置实例
Main->>Main : 启动HTTP服务器
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L108-L134)
- [main.go](file://cmd/server/main.go#L27-L32)
**Section sources**
- [config.go](file://pkg/config/config.go#L26-L32)
- [manager.go](file://pkg/config/manager.go#L19-L28)

View File

@@ -1,469 +0,0 @@
# 配置管理
<cite>
**本文档引用的文件**
- [config.go](file://pkg/config/config.go)
- [manager.go](file://pkg/config/manager.go)
- [main.go](file://cmd/server/main.go)
- [check-env.sh](file://scripts/check-env.sh)
- [dev.sh](file://scripts/dev.sh)
- [system_config.go](file://internal/model/system_config.go)
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
</cite>
## 目录
1. [简介](#简介)
2. [配置系统架构](#配置系统架构)
3. [配置加载流程](#配置加载流程)
4. [可配置选项详解](#可配置选项详解)
5. [默认配置与覆盖机制](#默认配置与覆盖机制)
6. [配置文件编写指南](#配置文件编写指南)
7. [动态配置与环境特定配置](#动态配置与环境特定配置)
8. [配置验证与最佳实践](#配置验证与最佳实践)
9. [结论](#结论)
## 简介
CarrotSkin项目的配置管理系统采用Viper库实现提供了一套完整的配置管理解决方案。该系统支持从环境变量、.env文件等多种来源加载配置确保了应用在不同环境下的灵活性和可移植性。配置系统设计遵循了安全性和易用性的原则为开发者提供了清晰的配置管理接口。
本系统的主要特点包括:
- 基于Viper库的配置管理
- 支持环境变量和.env文件
- 线程安全的单例模式
- 丰富的默认配置值
- 灵活的配置覆盖机制
**Section sources**
- [config.go](file://pkg/config/config.go#L1-L305)
- [manager.go](file://pkg/config/manager.go#L1-L68)
## 配置系统架构
CarrotSkin的配置系统采用分层架构设计核心组件包括配置结构体、配置管理器和配置加载器。系统通过Viper库实现配置的解析和管理确保了配置的一致性和可靠性。
```mermaid
graph TD
A[环境变量] --> B[Viper配置管理器]
C[.env文件] --> B
B --> D[Config结构体]
D --> E[ServerConfig]
D --> F[DatabaseConfig]
D --> G[RedisConfig]
D --> H[RustFSConfig]
D --> I[JWTConfig]
D --> J[LogConfig]
D --> K[UploadConfig]
D --> L[EmailConfig]
M[配置管理器] --> D
M --> N[全局配置实例]
O[应用程序] --> M
style A fill:#f9f,stroke:#333
style C fill:#f9f,stroke:#333
style B fill:#bbf,stroke:#333
style D fill:#ff9,stroke:#333
style M fill:#9f9,stroke:#333
style O fill:#f96,stroke:#333
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L14-L24)
- [manager.go](file://pkg/config/manager.go#L8-L17)
**Section sources**
- [config.go](file://pkg/config/config.go#L1-L305)
- [manager.go](file://pkg/config/manager.go#L1-L68)
## 配置加载流程
CarrotSkin的配置加载流程是一个多步骤的过程确保配置能够正确地从各种来源加载并应用。加载流程包括环境变量前缀设置、默认值配置、环境变量映射和最终的配置解析。
```mermaid
flowchart TD
Start([开始]) --> LoadDotEnv["加载.env文件"]
LoadDotEnv --> SetDefaults["设置默认值"]
SetDefaults --> SetPrefix["设置环境变量前缀 CARROTSKIN"]
SetPrefix --> EnableAutoEnv["启用自动环境变量"]
EnableAutoEnv --> SetupMappings["设置环境变量映射"]
SetupMappings --> UnmarshalConfig["解析配置到结构体"]
UnmarshalConfig --> OverrideEnv["从环境变量覆盖特殊配置"]
OverrideEnv --> ReturnConfig["返回配置实例"]
ReturnConfig --> End([结束])
style Start fill:#4CAF50,stroke:#333
style End fill:#4CAF50,stroke:#333
style LoadDotEnv fill:#2196F3,stroke:#333
style SetDefaults fill:#2196F3,stroke:#333
style SetPrefix fill:#2196F3,stroke:#333
style EnableAutoEnv fill:#2196F3,stroke:#333
style SetupMappings fill:#2196F3,stroke:#333
style UnmarshalConfig fill:#2196F3,stroke:#333
style OverrideEnv fill:#2196F3,stroke:#333
style ReturnConfig fill:#2196F3,stroke:#333
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L108-L132)
**Section sources**
- [config.go](file://pkg/config/config.go#L108-L132)
## 可配置选项详解
CarrotSkin项目提供了丰富的可配置选项涵盖了服务器、数据库、Redis、对象存储、JWT、日志、文件上传和邮件等多个模块。每个模块都有详细的配置参数满足不同场景的需求。
### 服务器配置
服务器配置控制应用的基本运行参数,包括端口、模式和超时设置。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| server.port | SERVER_PORT | string | 服务器监听端口 | :8080 |
| server.mode | SERVER_MODE | string | 运行模式 (debug/production) | debug |
| server.read_timeout | SERVER_READ_TIMEOUT | duration | 读取超时时间 | 30s |
| server.write_timeout | SERVER_WRITE_TIMEOUT | duration | 写入超时时间 | 30s |
**Section sources**
- [config.go](file://pkg/config/config.go#L26-L32)
### 数据库配置
数据库配置管理与PostgreSQL数据库的连接参数和连接池设置。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| database.driver | DATABASE_DRIVER | string | 数据库驱动 | postgres |
| database.host | DATABASE_HOST | string | 数据库主机地址 | localhost |
| database.port | DATABASE_PORT | int | 数据库端口 | 5432 |
| database.username | DATABASE_USERNAME | string | 数据库用户名 | - |
| database.password | DATABASE_PASSWORD | string | 数据库密码 | - |
| database.database | DATABASE_NAME | string | 数据库名称 | - |
| database.ssl_mode | DATABASE_SSL_MODE | string | SSL模式 | disable |
| database.timezone | DATABASE_TIMEZONE | string | 时区设置 | Asia/Shanghai |
| database.max_idle_conns | DATABASE_MAX_IDLE_CONNS | int | 最大空闲连接数 | 10 |
| database.max_open_conns | DATABASE_MAX_OPEN_CONNS | int | 最大打开连接数 | 100 |
| database.conn_max_lifetime | DATABASE_CONN_MAX_LIFETIME | duration | 连接最大生命周期 | 1h |
**Section sources**
- [config.go](file://pkg/config/config.go#L34-L47)
### Redis配置
Redis配置管理Redis缓存服务的连接信息和连接池设置。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| redis.host | REDIS_HOST | string | Redis主机地址 | localhost |
| redis.port | REDIS_PORT | int | Redis端口 | 6379 |
| redis.password | REDIS_PASSWORD | string | Redis密码 | - |
| redis.database | REDIS_DATABASE | int | Redis数据库编号 | 0 |
| redis.pool_size | REDIS_POOL_SIZE | int | 连接池大小 | 10 |
**Section sources**
- [config.go](file://pkg/config/config.go#L49-L56)
### 对象存储(RustFS)配置
对象存储配置管理S3兼容的对象存储服务(RustFS)的连接信息和存储桶设置。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| rustfs.endpoint | RUSTFS_ENDPOINT | string | 对象存储端点 | 127.0.0.1:9000 |
| rustfs.access_key | RUSTFS_ACCESS_KEY | string | 访问密钥 | - |
| rustfs.secret_key | RUSTFS_SECRET_KEY | string | 密钥 | - |
| rustfs.use_ssl | RUSTFS_USE_SSL | bool | 是否使用SSL | false |
| rustfs.buckets | RUSTFS_BUCKET_* | map[string]string | 存储桶映射 | - |
**Section sources**
- [config.go](file://pkg/config/config.go#L58-L65)
### JWT配置
JWT配置管理JSON Web Token的密钥和过期时间设置。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| jwt.secret | JWT_SECRET | string | JWT密钥 | - |
| jwt.expire_hours | JWT_EXPIRE_HOURS | int | JWT过期小时数 | 168(7天) |
**Section sources**
- [config.go](file://pkg/config/config.go#L67-L71)
### 日志配置
日志配置管理应用日志的级别、格式和文件滚动设置。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| log.level | LOG_LEVEL | string | 日志级别 | info |
| log.format | LOG_FORMAT | string | 日志格式 | json |
| log.output | LOG_OUTPUT | string | 日志输出路径 | logs/app.log |
| log.max_size | LOG_MAX_SIZE | int | 单个日志文件最大大小(MB) | 100 |
| log.max_backups | LOG_MAX_BACKUPS | int | 保留旧日志文件的最大个数 | 3 |
| log.max_age | LOG_MAX_AGE | int | 保留旧日志文件的最大天数 | 28 |
| log.compress | LOG_COMPRESS | bool | 是否压缩归档日志 | true |
**Section sources**
- [config.go](file://pkg/config/config.go#L79-L88)
### 文件上传配置
文件上传配置管理文件上传的大小限制和允许的文件类型。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| upload.max_size | UPLOAD_MAX_SIZE | int64 | 最大上传文件大小 | 10MB |
| upload.allowed_types | - | []string | 允许的文件MIME类型 | ["image/png", "image/jpeg"] |
| upload.texture_max_size | UPLOAD_TEXTURE_MAX_SIZE | int64 | 纹理文件最大大小 | 2MB |
| upload.avatar_max_size | UPLOAD_AVATAR_MAX_SIZE | int64 | 头像文件最大大小 | 1MB |
**Section sources**
- [config.go](file://pkg/config/config.go#L90-L96)
### 邮件配置
邮件配置管理SMTP邮件服务的连接信息和发件人设置。
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|--------|----------|------|------|--------|
| email.enabled | EMAIL_ENABLED | bool | 是否启用邮件服务 | false |
| email.smtp_host | EMAIL_SMTP_HOST | string | SMTP主机地址 | - |
| email.smtp_port | EMAIL_SMTP_PORT | int | SMTP端口 | 587 |
| email.username | EMAIL_USERNAME | string | SMTP用户名 | - |
| email.password | EMAIL_PASSWORD | string | SMTP密码 | - |
| email.from_name | EMAIL_FROM_NAME | string | 发件人名称 | - |
**Section sources**
- [config.go](file://pkg/config/config.go#L98-L106)
## 默认配置与覆盖机制
CarrotSkin的配置系统采用分层覆盖机制确保配置的灵活性和可靠性。系统首先设置合理的默认值然后通过环境变量进行覆盖最后处理特殊的配置覆盖逻辑。
### 默认配置设置
系统通过`setDefaults()`函数设置所有配置项的默认值,确保在没有提供外部配置时应用仍能正常运行。
```mermaid
classDiagram
class Config {
+Server ServerConfig
+Database DatabaseConfig
+Redis RedisConfig
+RustFS RustFSConfig
+JWT JWTConfig
+Casbin CasbinConfig
+Log LogConfig
+Upload UploadConfig
+Email EmailConfig
}
class ServerConfig {
+Port string
+Mode string
+ReadTimeout time.Duration
+WriteTimeout time.Duration
}
class DatabaseConfig {
+Driver string
+Host string
+Port int
+Username string
+Password string
+Database string
+SSLMode string
+Timezone string
+MaxIdleConns int
+MaxOpenConns int
+ConnMaxLifetime time.Duration
}
class RedisConfig {
+Host string
+Port int
+Password string
+Database int
+PoolSize int
}
class RustFSConfig {
+Endpoint string
+AccessKey string
+SecretKey string
+UseSSL bool
+Buckets map[string]string
}
class JWTConfig {
+Secret string
+ExpireHours int
}
class LogConfig {
+Level string
+Format string
+Output string
+MaxSize int
+MaxBackups int
+MaxAge int
+Compress bool
}
Config "1" *-- "1" ServerConfig
Config "1" *-- "1" DatabaseConfig
Config "1" *-- "1" RedisConfig
Config "1" *-- "1" RustFSConfig
Config "1" *-- "1" JWTConfig
Config "1" *-- "1" LogConfig
Config "1" *-- "1" UploadConfig
Config "1" *-- "1" EmailConfig
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L13-L24)
**Section sources**
- [config.go](file://pkg/config/config.go#L135-L188)
### 配置覆盖流程
配置覆盖流程确保了配置的优先级顺序:环境变量 > .env文件 > 默认值。系统通过`overrideFromEnv()`函数处理特殊的配置覆盖逻辑。
```mermaid
sequenceDiagram
participant App as 应用程序
participant ConfigMgr as 配置管理器
participant Viper as Viper库
participant Env as 环境变量
participant DotEnv as .env文件
App->>ConfigMgr : 调用 config.Init()
ConfigMgr->>ConfigMgr : 执行 once.Do()
ConfigMgr->>DotEnv : 加载 .env 文件
ConfigMgr->>Viper : 设置默认值
ConfigMgr->>Viper : 设置环境变量前缀 CARROTSKIN
ConfigMgr->>Viper : 启用自动环境变量
ConfigMgr->>Viper : 绑定环境变量映射
Viper->>Env : 读取环境变量
Viper->>Viper : 解析配置到结构体
ConfigMgr->>Env : 从环境变量覆盖特殊配置
ConfigMgr->>App : 返回配置实例
```
**Diagram sources**
- [config.go](file://pkg/config/config.go#L108-L132)
- [config.go](file://pkg/config/config.go#L238-L304)
**Section sources**
- [config.go](file://pkg/config/config.go#L238-L304)
## 配置文件编写指南
为帮助初学者快速上手,以下是配置文件的编写指南。
### .env文件示例
创建`.env`文件并填入以下内容:
```bash
# 数据库配置
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=carrot
DATABASE_PASSWORD=secret
DATABASE_NAME=carrotskin
DATABASE_SSL_MODE=disable
# 对象存储配置
RUSTFS_ENDPOINT=127.0.0.1:9000
RUSTFS_ACCESS_KEY=minioadmin
RUSTFS_SECRET_KEY=minioadmin
RUSTFS_USE_SSL=false
# JWT配置
JWT_SECRET=your-jwt-secret-key-change-this-in-production
# 服务器配置
SERVER_PORT=:8080
SERVER_MODE=debug
# Redis配置
REDIS_HOST=localhost
REDIS_PORT=6379
```
### 配置验证脚本
使用提供的`check-env.sh`脚本验证配置的完整性:
```bash
./scripts/check-env.sh
```
该脚本会检查必需的环境变量是否设置,并提供配置概览和安全建议。
**Section sources**
- [check-env.sh](file://scripts/check-env.sh#L1-L78)
- [dev.sh](file://scripts/dev.sh#L1-L29)
## 动态配置与环境特定配置
对于经验丰富的开发者CarrotSkin提供了动态配置和环境特定配置的最佳实践。
### 环境特定配置
通过环境变量前缀`CARROTSKIN`,可以在不同环境中使用不同的配置:
```bash
# 开发环境
CARROTSKIN_SERVER_PORT=:8080 \
CARROTSKIN_DATABASE_HOST=localhost \
CARROTSKIN_REDIS_HOST=localhost \
go run cmd/server/main.go
# 生产环境
CARROTSKIN_SERVER_PORT=:80 \
CARROTSKIN_DATABASE_HOST=prod-db.example.com \
CARROTSKIN_REDIS_HOST=prod-redis.example.com \
go run cmd/server/main.go
```
### 动态配置管理
系统提供了线程安全的配置访问接口,支持在运行时安全地获取配置:
```go
// 获取配置实例
cfg, err := config.GetConfig()
if err != nil {
log.Fatalf("配置获取失败: %v", err)
}
// 或使用panic方式获取确保配置已初始化
cfg := config.MustGetConfig()
// 获取特定模块配置
rustFSConfig := config.MustGetRustFSConfig()
```
**Section sources**
- [manager.go](file://pkg/config/manager.go#L19-L63)
- [main.go](file://cmd/server/main.go#L27-L124)
## 配置验证与最佳实践
### 配置验证
CarrotSkin提供了多种配置验证机制确保配置的正确性和安全性
1. **必需变量检查**:通过`check-env.sh`脚本检查必需的环境变量
2. **配置合理性检查**检查JWT密钥长度、数据库密码等安全相关配置
3. **运行时验证**:在应用启动时验证配置的有效性
### 最佳实践
1. **使用.env文件管理开发配置**:避免将敏感信息硬编码在代码中
2. **设置强密码和密钥**确保JWT密钥至少32字符使用随机字符串
3. **环境隔离**:为不同环境(开发、测试、生产)使用不同的配置
4. **配置备份**:定期备份重要的配置文件
5. **监控配置变更**:记录配置变更历史,便于问题追踪
**Section sources**
- [check-env.sh](file://scripts/check-env.sh#L1-L78)
## 结论
CarrotSkin的配置管理系统提供了一套完整、灵活且安全的配置管理解决方案。通过Viper库的强大功能系统能够从多种来源加载配置并提供了丰富的默认值和灵活的覆盖机制。配置系统的设计考虑了不同用户的需求既为初学者提供了简单的配置文件编写指南又为经验丰富的开发者提供了动态配置和环境特定配置的最佳实践。
该系统的线程安全设计和单例模式确保了配置的一致性和可靠性而详细的配置选项和验证机制则保证了应用的稳定运行。通过遵循本文档提供的指南和最佳实践开发者可以有效地管理和维护CarrotSkin应用的配置确保其在不同环境下的正常运行。

View File

@@ -1,266 +0,0 @@
# 项目概述
<cite>
**本文档引用的文件**
- [carrotskin](file://go.mod#L1-L92)
- [routes.go](file://internal/handler/routes.go#L1-L140)
- [user.go](file://internal/model/user.go#L1-L71)
- [profile.go](file://internal/model/profile.go#L1-L64)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
- [config.go](file://pkg/config/config.go#L1-L305)
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [manager.go](file://pkg/auth/manager.go#L1-L46)
- [user_service.go](file://internal/service/user_service.go#L1-L249)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构概述](#架构概述)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排除指南](#故障排除指南)
9. [结论](#结论)
## 简介
CarrotSkin 是一个为 Minecraft 玩家提供皮肤管理服务的后端系统。该项目旨在为用户提供一个稳定、安全且功能丰富的平台,用于上传、管理和分享 Minecraft 皮肤与披风。系统支持用户认证、材质管理、档案系统以及与 Yggdrasil 协议的集成,确保与 Minecraft 客户端的无缝对接。通过 Gin 框架构建的分层架构CarrotSkin 实现了清晰的职责分离,便于维护和扩展。
## 项目结构
CarrotSkin 项目采用模块化设计,主要分为 `internal``pkg` 两个目录。`internal` 目录包含应用的核心逻辑包括处理器handler、中间件middleware、模型model、仓库repository和服务service`pkg` 目录则封装了可重用的工具和第三方服务集成如数据库、Redis、对象存储等。这种结构有助于保持代码的整洁和可维护性。
```mermaid
graph TB
subgraph "内部模块"
Handler[handler]
Middleware[middleware]
Model[model]
Repository[repository]
Service[service]
end
subgraph "公共包"
Config[config]
Database[database]
Redis[redis]
Storage[storage]
Auth[auth]
Logger[logger]
Utils[utils]
end
Handler --> Service
Service --> Repository
Repository --> Database
Repository --> Redis
Repository --> Storage
Auth --> Middleware
```
**图源**
- [go.mod](file://go.mod#L1-L92)
- [routes.go](file://internal/handler/routes.go#L1-L140)
**本节来源**
- [go.mod](file://go.mod#L1-L92)
- [routes.go](file://internal/handler/routes.go#L1-L140)
## 核心组件
CarrotSkin 的核心组件包括用户认证、材质管理、档案系统和 Yggdrasil 协议集成。用户认证模块负责用户的注册、登录和权限验证,使用 JWT 进行安全的会话管理。材质管理模块允许用户上传、搜索和下载皮肤与披风,支持收藏和下载统计。档案系统为每个用户提供了多个 Minecraft 角色档案支持设置活跃档案和管理皮肤与披风。Yggdrasil 协议集成确保了与 Minecraft 客户端的兼容性,支持身份验证和服务器加入。
**本节来源**
- [user.go](file://internal/model/user.go#L1-L71)
- [profile.go](file://internal/model/profile.go#L1-L64)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
## 架构概述
CarrotSkin 采用基于 Gin 框架的分层架构分为处理器Handler、服务Service和仓库Repository三层。处理器层负责处理 HTTP 请求和响应,服务层实现业务逻辑,仓库层负责数据访问。这种分层设计确保了各层之间的职责分离,提高了代码的可测试性和可维护性。系统与 PostgreSQL、Redis 和 MinIO/RustFS 等外部系统集成,分别用于持久化存储、缓存和对象存储。
```mermaid
graph TD
Client[客户端] --> Handler[处理器]
Handler --> Service[服务]
Service --> Repository[仓库]
Repository --> PostgreSQL[(PostgreSQL)]
Repository --> Redis[(Redis)]
Repository --> MinIO[(MinIO/RustFS)]
```
**图源**
- [routes.go](file://internal/handler/routes.go#L1-L140)
- [config.go](file://pkg/config/config.go#L1-L305)
**本节来源**
- [routes.go](file://internal/handler/routes.go#L1-L140)
- [config.go](file://pkg/config/config.go#L1-L305)
## 详细组件分析
### 用户认证分析
用户认证模块是 CarrotSkin 的安全基石,负责用户的注册、登录和权限验证。通过 JWT 实现无状态的会话管理,确保了系统的可扩展性。用户信息存储在 PostgreSQL 中密码经过哈希处理以保证安全性。Redis 用于缓存用户会话和验证码,提高响应速度。
#### 类图
```mermaid
classDiagram
class User {
+int64 ID
+string Username
+string Email
+string Avatar
+int Points
+string Role
+int16 Status
+string Properties
+*time.Time LastLoginAt
+time.Time CreatedAt
+time.Time UpdatedAt
}
class Token {
+int64 ID
+int64 UserID
+string AccessToken
+string RefreshToken
+string ProfileId
+time.Time ExpiresAt
+time.Time CreatedAt
}
class Yggdrasil {
+int64 ID
+string Password
}
User --> Token : "拥有"
User --> Yggdrasil : "关联"
```
**图源**
- [user.go](file://internal/model/user.go#L1-L71)
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
**本节来源**
- [user_service.go](file://internal/service/user_service.go#L1-L249)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
### 材质管理分析
材质管理模块允许用户上传、搜索和下载 Minecraft 皮肤与披风。每个材质记录包含上传者信息、名称、描述、类型、URL、哈希值等元数据。系统通过 MinIO/RustFS 存储实际的材质文件PostgreSQL 存储元数据。Redis 用于缓存热门材质和下载统计,提高性能。
#### 序列图
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "处理器"
participant Service as "服务"
participant Repository as "仓库"
participant MinIO as "MinIO/RustFS"
Client->>Handler : 上传材质
Handler->>Service : 调用CreateTexture
Service->>Repository : 检查哈希是否存在
Repository->>PostgreSQL : 查询数据库
Repository-->>Service : 返回结果
Service->>MinIO : 生成预签名URL
MinIO-->>Service : 返回URL
Service-->>Handler : 返回上传URL
Handler-->>Client : 返回预签名URL
```
**图源**
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture.go](file://internal/model/texture.go#L1-L77)
### 档案系统分析
档案系统为每个用户提供了多个 Minecraft 角色档案,支持设置活跃档案和管理皮肤与披风。每个档案包含 UUID、用户名、皮肤和披风的引用。系统通过 RSA 密钥对确保档案的安全性,支持 Yggdrasil 协议的身份验证。
#### 流程图
```mermaid
flowchart TD
Start([创建档案]) --> ValidateUser["验证用户存在"]
ValidateUser --> CheckName["检查角色名是否已存在"]
CheckName --> GenerateUUID["生成UUID"]
GenerateUUID --> GenerateKey["生成RSA密钥对"]
GenerateKey --> CreateProfile["创建档案记录"]
CreateProfile --> SetActive["设置为活跃档案"]
SetActive --> End([档案创建成功])
```
**图源**
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile.go](file://internal/model/profile.go#L1-L64)
**本节来源**
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile.go](file://internal/model/profile.go#L1-L64)
### Yggdrasil 协议集成分析
Yggdrasil 协议集成确保了 CarrotSkin 与 Minecraft 客户端的兼容性。系统实现了身份验证、令牌验证、刷新和服务器加入等功能。通过 Redis 存储会话信息,确保玩家在加入服务器时的身份验证。
#### 序列图
```mermaid
sequenceDiagram
participant Client as "Minecraft客户端"
participant Handler as "处理器"
participant Service as "服务"
participant Repository as "仓库"
participant Redis as "Redis"
Client->>Handler : authenticate
Handler->>Service : 调用Authenticate
Service->>Repository : 验证用户凭证
Repository->>PostgreSQL : 查询用户
Repository-->>Service : 返回用户信息
Service->>Redis : 存储会话数据
Redis-->>Service : 确认存储
Service-->>Handler : 返回认证结果
Handler-->>Client : 返回访问令牌
```
**图源**
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [redis.go](file://pkg/redis/redis.go#L1-L175)
**本节来源**
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
## 依赖分析
CarrotSkin 依赖于多个外部系统和库,包括 PostgreSQL 用于持久化存储Redis 用于缓存和会话管理MinIO/RustFS 用于对象存储。Gin 框架提供了高效的 HTTP 路由和中间件支持GORM 简化了数据库操作。JWT 用于安全的用户认证Viper 用于配置管理。
```mermaid
graph TD
CarrotSkin --> PostgreSQL
CarrotSkin --> Redis
CarrotSkin --> MinIO
CarrotSkin --> Gin
CarrotSkin --> GORM
CarrotSkin --> JWT
CarrotSkin --> Viper
```
**图源**
- [go.mod](file://go.mod#L1-L92)
- [config.go](file://pkg/config/config.go#L1-L305)
**本节来源**
- [go.mod](file://go.mod#L1-L92)
- [config.go](file://pkg/config/config.go#L1-L305)
## 性能考虑
CarrotSkin 在设计时充分考虑了性能优化。通过 Redis 缓存频繁访问的数据如用户会话、验证码和热门材质减少了数据库的负载。MinIO/RustFS 提供了高效的对象存储支持大规模文件上传和下载。Gin 框架的高性能特性确保了快速的请求处理。此外,系统通过分页和限制查询结果数量,避免了大数据量查询对性能的影响。
## 故障排除指南
在使用 CarrotSkin 时可能会遇到一些常见问题。例如用户无法登录可能是由于密码错误或账户被禁用。材质上传失败可能是由于文件大小超出限制或存储桶配置错误。Yggdrasil 身份验证失败可能是由于令牌过期或会话数据不匹配。建议检查日志文件以获取详细的错误信息,并确保所有外部服务(如 PostgreSQL、Redis 和 MinIO正常运行。
**本节来源**
- [user_service.go](file://internal/service/user_service.go#L1-L249)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
## 结论
CarrotSkin 是一个功能丰富且安全可靠的 Minecraft 皮肤站后端服务。通过分层架构和模块化设计,系统实现了清晰的职责分离,便于维护和扩展。与 PostgreSQL、Redis 和 MinIO/RustFS 的集成确保了数据的持久性和高性能。Yggdrasil 协议的支持使得系统能够无缝对接 Minecraft 客户端。未来,可以进一步优化性能,增加更多的用户功能,如皮肤编辑器和社区互动。

File diff suppressed because one or more lines are too long

View File

@@ -1,74 +0,0 @@
# ==================== 构建阶段 ====================
FROM golang:latest AS builder
# 安装构建依赖
RUN apk add --no-cache git ca-certificates tzdata
# 设置工作目录
WORKDIR /build
# 复制依赖文件
COPY go.mod go.sum ./
# 配置 Go 代理并下载依赖
ENV GOPROXY=https://goproxy.cn,direct
RUN go mod download
# 复制源代码
COPY . .
# 构建应用
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s -X main.Version=$(git describe --tags --always --dirty 2>/dev/null || echo 'dev')" \
-o server ./cmd/server
# ==================== 运行阶段 ====================
FROM alpine:3.19
# 安装运行时依赖
RUN apk add --no-cache ca-certificates tzdata
# 设置时区
ENV TZ=Asia/Shanghai
# 创建非 root 用户
RUN adduser -D -g '' appuser
# 设置工作目录
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /build/server .
# 复制配置文件目录结构
COPY --from=builder /build/configs ./configs
# 设置文件权限
RUN chown -R appuser:appuser /app
# 切换到非 root 用户
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1
# 启动应用
ENTRYPOINT ["./server"]

646
README.md
View File

@@ -1,564 +1,160 @@
# CarrotSkin Backend # CarrotSkin Backend
一个功能完善的Minecraft皮肤站后端系统,采用单体架构设计基于Go语言和Gin框架开发 一个功能完善的 Minecraft 皮肤站后端,基于 Go + Gin 构建,覆盖用户认证、材质管理、角色档案、审计日志等核心能力,并提供完整的 Swagger 文档与容器友好的环境变量配置
## ✨ 核心功能 ## ✨ 功能亮点
- **用户认证系统** - 注册、登录、JWT认证、积分系统 - **账号体系**:注册 / 登录 / JWT 鉴权 / Yggdrasil 密码同步 / 用户积分
- **邮箱验证系统** - 注册验证、找回密码、更换邮箱基于Redis的验证码 - **邮箱验证码**:验证码发送频率控制、邮箱绑定与变更
- **材质管理系统** - 皮肤/披风上传、搜索、收藏、下载统计 - **材质中心**皮肤/披风上传、搜索、收藏、下载统计、Hash 去重
- **角色档案系统** - Minecraft角色创建、管理、RSA密钥生成 - **角色档案**Minecraft Profile 管理、RSA 密钥生成、活跃档案切换
- **文件存储** - MinIO/RustFS对象存储集成、预签名URL上传 - **存储与上传**RustFS/MinIO 预签名 URL,减轻服务器带宽压力
- **缓存系统** - Redis缓存、验证码存储、频率限制 - **任务与日志**:登录日志、操作审计、材质下载记录、定时任务
- **权限管理** - Casbin RBAC权限控制 - **权限体系**Casbin RBAC,支持细粒度路线授
- **数据审计** - 登录日志、操作审计、下载记录 - **配置管理**100% 依赖环境变量,`SERVER_SWAGGER_ENABLED` 控制 Swagger
- **可观测性**Zap 结构化日志、统一 API 响应模型
## 项目结构 ## 🛠 技术栈
| 类型 | 选型 |
| --- | --- |
| 语言 / 运行时 | Go 1.24+ |
| Web 框架 | Gin |
| ORM | GORM (PostgreSQL 驱动) |
| 数据库 | PostgreSQL 15+ |
| 缓存 / 消息 | Redis 6+ |
| 对象存储 | RustFS / MinIOS3 兼容) |
| 权限控制 | Casbin |
| 配置 | Viper + `.env` |
| API 文档 | swaggo / Swagger UI |
| 日志 | Uber Zap |
## 📁 目录结构
``` ```
backend/ backend/
├── cmd/ # 应用程序入口 ├── cmd/server/ # 应用入口main.go
│ └── server/ # 主服务器入口 ├── internal/
└── main.go # 服务初始化、路由注册 ├── handler/ # HTTP Handler 与 Swagger 注解
├── internal/ # 私有应用代码 │ ├── service/ # 业务逻辑
│ ├── handler/ # HTTP处理器函数式 │ ├── repository/ # 数据访问
│ ├── routes.go # 路由注册 │ ├── model/ # GORM 数据模型
│ ├── auth_handler.go │ ├── types/ # 请求/响应 DTO
│ ├── user_handler.go │ ├── middleware/ # Gin 中间件
│ └── ... └── task/ # 定时任务与后台作业
│ ├── service/ # 业务逻辑服务(函数式 ├── pkg/ # 可复用组件config、database、auth、logger、redis、storage 等
├── common.go # 公共声明jsoniter等 ├── docs/ # swagger 生成产物docs.go / swagger.json / swagger.yaml
├── user_service.go ├── start.sh # 启动脚本(自动 swag init
└── ... ├── docker-compose.yml # 本地容器编排
│ ├── repository/ # 数据访问层(函数式) ├── .env.example # 环境变量示例
├── user_repository.go └── go.mod # Go Module 定义
│ │ └── ...
│ ├── model/ # 数据模型GORM
│ ├── middleware/ # 中间件
│ └── types/ # 类型定义
├── pkg/ # 公共库代码
│ ├── auth/ # 认证授权
│ │ └── manager.go # JWT服务管理器
│ ├── config/ # 配置管理
│ │ └── manager.go # 配置管理器
│ ├── database/ # 数据库连接
│ │ ├── manager.go # 数据库管理器AutoMigrate
│ │ └── postgres.go # PostgreSQL连接
│ ├── email/ # 邮件服务
│ │ └── manager.go # 邮件服务管理器
│ ├── logger/ # 日志系统
│ │ └── manager.go # 日志管理器
│ ├── redis/ # Redis客户端
│ │ └── manager.go # Redis管理器
│ ├── storage/ # 文件存储(RustFS/MinIO)
│ │ └── manager.go # 存储管理器
│ ├── utils/ # 工具函数
│ └── validator/ # 数据验证
├── docs/ # API定义和文档Swagger
├── configs/ # 配置文件
│ └── casbin/ # Casbin权限配置
├── go.mod # Go模块依赖
├── go.sum # Go模块校验
├── start.sh # Linux/Mac启动脚本
├── .env # 环境变量配置
└── README.md # 项目说明
``` ```
## 技术栈 ## ✅ 前置要求
- **语言**: Go 1.23+ - Go 1.24+
- **框架**: Gin Web Framework - PostgreSQL 15+
- **数据库**: PostgreSQL 15+ (GORM ORM) - Redis 6+
- **缓存**: Redis 6.0+ - RustFS / MinIO或其他兼容 S3 的对象存储,用于皮肤与头像)
- **存储**: RustFS/MinIO (S3兼容对象存储)
- **权限**: Casbin RBAC
- **日志**: Zap (结构化日志)
- **配置**: 环境变量 (.env) + Viper
- **JSON**: jsoniter (高性能JSON序列化)
- **文档**: Swagger/OpenAPI 3.0
## 快速开始 ## 🚀 快速开始
### 环境要求 1. **克隆仓库**
```bash
- Go 1.21或更高版本 git clone <repo>
- PostgreSQL 15或更高版本 cd backend
- Redis 6.0或更高版本 ```
- RustFS 或其他 S3 兼容对象存储服务
### 安装和运行
1. **克隆项目**
```bash
git clone <repository-url>
cd CarrotSkin/backend
```
2. **安装依赖** 2. **安装依赖**
```bash ```bash
go mod download go mod download
``` ```
3. **配置环境** 3. **配置环境变量**
```bash ```bash
# 复制环境变量文件 cp .env.example .env
cp .env.example .env # 根据实际环境填写数据库、Redis、对象存储、邮件等信息
# 编辑 .env 文件配置数据库、RustFS等服务连接信息 ```
```
**注意**:项目完全依赖 `.env` 文件进行配置,不再使用 YAML 配置文件,便于 Docker 容器化部署。
4. **初始化数据库** 4. **初始化数据库**
```bash ```bash
# 创建数据库 createdb carrotskin
createdb carrotskin # 或 psql -c "CREATE DATABASE carrotskin;"
# 或者使用PostgreSQL客户端 ```
psql -h localhost -U postgres -c "CREATE DATABASE carrotskin;" > 应用启动时会执行 `AutoMigrate`,自动创建 / 更新表结构。
```
> 💡 **提示**: 项目使用 GORM 的 `AutoMigrate` 功能自动创建和更新数据库表结构无需手动执行SQL脚本。首次启动时会自动创建所有表。 5. **启动服务**
- **推荐**`./start.sh`(自动 `swag init`,随后 `go run cmd/server/main.go`
- **手动启动**
```bash
swag init -g cmd/server/main.go -o docs
go run cmd/server/main.go
```
5. **运行服务** 6. **访问接口**
- API Root: `http://localhost:8080`
- Swagger: `http://localhost:8080/swagger/index.html`(需 `SERVER_SWAGGER_ENABLED=true`
方式一:使用启动脚本(推荐) ## ⚙️ 关键环境变量
```bash
# Linux/Mac
chmod +x start.sh
./start.sh
# Windows | 变量 | 说明 | 示例 |
start.bat | --- | --- | --- |
``` | `SERVER_PORT` | 服务监听端口 | `8080` |
| `SERVER_MODE` | Gin 模式debug/release | `debug` |
| `SERVER_SWAGGER_ENABLED` | 是否暴露 Swagger UI | `true` |
| `DATABASE_HOST` / `DATABASE_PORT` | PostgreSQL 地址 | `localhost` / `5432` |
| `DATABASE_USERNAME` / `DATABASE_PASSWORD` | 数据库凭据 | `postgres` |
| `DATABASE_NAME` | 数据库名称 | `carrotskin` |
| `REDIS_HOST` / `REDIS_PORT` | Redis 地址 | `localhost` / `6379` |
| `REDIS_PASSWORD` | Redis 密码(无可为空) | `` |
| `RUSTFS_ENDPOINT` | RustFS/MinIO 访问地址 | `127.0.0.1:9000` |
| `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` | 对象存储凭据 | `minioadmin` |
| `RUSTFS_BUCKET_TEXTURES` / `RUSTFS_BUCKET_AVATARS` | 存储桶名称 | `carrotskin-textures` |
| `JWT_SECRET` | JWT 签名密钥 | `change-me` |
| `EMAIL_ENABLED` | 是否开启邮件服务 | `true` |
| `EMAIL_SMTP_HOST` / `EMAIL_SMTP_PORT` | SMTP 配置 | `smtp.example.com` / `587` |
方式二:直接运行 更多变量请参考 `.env.example` 与 `.env.docker.example`。
```bash
# 设置环境变量(或使用.env文件
export DATABASE_HOST=localhost
export DATABASE_PORT=5432
# ... 其他环境变量
# 运行服务 ## 🧪 常用命令
go run cmd/server/main.go
```
> 💡 **提示**:
> - 启动脚本会自动加载 `.env` 文件中的环境变量
> - 首次启动时会自动执行数据库迁移AutoMigrate
> - 如果对象存储未配置,服务仍可启动(相关功能不可用)
服务启动后:
- **服务地址**: http://localhost:8080
- **Swagger文档**: http://localhost:8080/swagger/index.html
- **健康检查**: http://localhost:8080/health
## API接口
### 认证相关
- `POST /api/v1/auth/register` - 用户注册(需邮箱验证码)
- `POST /api/v1/auth/login` - 用户登录(支持用户名/邮箱)
- `POST /api/v1/auth/send-code` - 发送验证码(注册/重置密码/更换邮箱)
- `POST /api/v1/auth/reset-password` - 重置密码(需验证码)
### 用户相关(需认证)
- `GET /api/v1/user/profile` - 获取用户信息
- `PUT /api/v1/user/profile` - 更新用户信息(头像、密码)
- `POST /api/v1/user/avatar/upload-url` - 生成头像上传URL
- `PUT /api/v1/user/avatar` - 更新头像
- `POST /api/v1/user/change-email` - 更换邮箱(需验证码)
### 材质管理
公开接口:
- `GET /api/v1/texture` - 搜索材质
- `GET /api/v1/texture/:id` - 获取材质详情
认证接口:
- `POST /api/v1/texture/upload-url` - 生成材质上传URL
- `POST /api/v1/texture` - 创建材质记录
- `PUT /api/v1/texture/:id` - 更新材质
- `DELETE /api/v1/texture/:id` - 删除材质
- `POST /api/v1/texture/:id/favorite` - 切换收藏状态
- `GET /api/v1/texture/my` - 我的材质列表
- `GET /api/v1/texture/favorites` - 我的收藏列表
### 角色档案
公开接口:
- `GET /api/v1/profile/:uuid` - 获取档案详情
认证接口:
- `POST /api/v1/profile` - 创建角色档案UUID由后端生成
- `GET /api/v1/profile` - 我的档案列表
- `PUT /api/v1/profile/:uuid` - 更新档案
- `DELETE /api/v1/profile/:uuid` - 删除档案
- `POST /api/v1/profile/:uuid/activate` - 设置活跃档案
### 系统配置
- `GET /api/v1/system/config` - 获取系统配置
## 配置管理
### 环境变量配置
项目**完全依赖环境变量**进行配置,不使用 YAML 配置文件,便于容器化部署:
1. **配置来源**: 环境变量 或 `.env` 文件
2. **环境变量格式**: 使用下划线分隔,全大写,如 `DATABASE_HOST`
3. **容器部署**: 直接在容器运行时设置环境变量即可
**主要环境变量**:
```bash
# 数据库配置
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=your_password
DATABASE_NAME=carrotskin
# Redis配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password
REDIS_DATABASE=0
REDIS_POOL_SIZE=10
# RustFS对象存储配置 (S3兼容)
RUSTFS_ENDPOINT=127.0.0.1:9000
RUSTFS_ACCESS_KEY=your_access_key
RUSTFS_SECRET_KEY=your_secret_key
RUSTFS_USE_SSL=false
RUSTFS_BUCKET_TEXTURES=carrot-skin-textures
RUSTFS_BUCKET_AVATARS=carrot-skin-avatars
# JWT配置
JWT_SECRET=your-jwt-secret-key
JWT_EXPIRE_HOURS=168
# 邮件配置
EMAIL_ENABLED=true
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_USERNAME=noreply@example.com
EMAIL_PASSWORD=your_email_password
EMAIL_FROM_NAME=CarrotSkin
```
**动态配置(存储在数据库中)**:
- 积分系统配置(注册奖励、签到积分等)
- 用户限制配置(最大材质数、最大角色数等)
- 网站设置(站点名称、公告、维护模式等)
完整的环境变量列表请参考 `.env.example` 文件。
### 数据库自动迁移
项目使用 GORM 的 `AutoMigrate` 功能自动管理数据库表结构:
- **首次启动**: 自动创建所有表结构
- **模型更新**: 自动添加新字段、索引等
- **类型转换**: 自动处理字段类型变更如枚举类型转为varchar
- **外键管理**: 自动管理外键关系
**注意事项**:
- 生产环境建议先备份数据库再执行迁移
- 某些复杂变更(如删除字段)可能需要手动处理
- 枚举类型在PostgreSQL中存储为varchar避免类型兼容问题
## 架构设计
### 面向过程的函数式架构
项目采用**面向过程的函数式架构**,摒弃不必要的面向对象抽象,使用独立函数和单例管理器模式,代码更简洁、可维护性更强:
```
┌─────────────────────────────────────┐
│ Handler 层 (函数) │ ← 路由处理、参数验证、响应格式化
├─────────────────────────────────────┤
│ Service 层 (函数) │ ← 业务逻辑、权限检查、数据验证
├─────────────────────────────────────┤
│ Repository 层 (函数) │ ← 数据库操作、关联查询
├─────────────────────────────────────┤
│ Manager 层 (单例模式) │ ← 核心依赖管理(线程安全)
│ - database.MustGetDB() │
│ - logger.MustGetLogger() │
│ - auth.MustGetJWTService() │
│ - redis.MustGetClient() │
│ - email.MustGetService() │
│ - storage.MustGetClient() │
│ - config.MustGetConfig() │
├──────────────┬──────────────────────┤
│ PostgreSQL │ Redis │ RustFS │ ← 数据存储层
└──────────────┴──────────────────────┘
```
### 架构特点
1. **函数式设计**: 所有业务逻辑以独立函数形式实现,无结构体方法,降低耦合度
2. **管理器模式**: 使用 `sync.Once` 实现线程安全的单例管理器,统一管理核心依赖
3. **按需获取**: 通过管理器函数按需获取依赖,避免链式传递,代码更清晰
4. **自动迁移**: 使用 GORM AutoMigrate 自动管理数据库表结构
5. **高性能**: 使用 jsoniter 替代标准库 json提升序列化性能
### 核心模块
1. **认证模块** (`internal/handler/auth_handler.go`)
- JWT令牌生成和验证通过 `auth.MustGetJWTService()` 获取)
- bcrypt密码加密
- 邮箱验证码注册
- 密码重置功能
- 登录日志记录(支持用户名/邮箱登录)
2. **用户模块** (`internal/handler/user_handler.go`)
- 用户信息管理
- 头像上传预签名URL通过 `storage.MustGetClient()` 获取)
- 密码修改(需原密码验证)
- 邮箱更换(需验证码)
- 积分系统
3. **邮箱验证模块** (`internal/service/verification_service.go`)
- 验证码生成6位数字
- 验证码存储Redis10分钟有效期通过 `redis.MustGetClient()` 获取)
- 发送频率限制1分钟
- 邮件发送HTML格式通过 `email.MustGetService()` 获取)
4. **材质模块** (`internal/handler/texture_handler.go`)
- 材质上传预签名URL
- 材质搜索和收藏
- Hash去重
- 下载统计
5. **档案模块** (`internal/handler/profile_handler.go`)
- Minecraft角色管理
- RSA密钥生成RSA-2048
- 活跃状态管理
- 档案数量限制
6. **管理器模块** (`pkg/*/manager.go`)
- 数据库管理器:`database.MustGetDB()` - 线程安全的数据库连接
- 日志管理器:`logger.MustGetLogger()` - 结构化日志实例
- JWT管理器`auth.MustGetJWTService()` - JWT服务实例
- Redis管理器`redis.MustGetClient()` - Redis客户端
- 邮件管理器:`email.MustGetService()` - 邮件服务
- 存储管理器:`storage.MustGetClient()` - 对象存储客户端
- 配置管理器:`config.MustGetConfig()` - 应用配置
### 技术特性
- **架构优势**:
- 面向过程的函数式设计,代码简洁清晰
- 单例管理器模式,线程安全的依赖管理
- 按需获取依赖,避免链式传递
- 自动数据库迁移AutoMigrate
- **安全性**:
- bcrypt密码加密、JWT令牌认证
- 邮箱验证码(注册/重置密码/更换邮箱)
- Casbin RBAC权限控制
- 频率限制(防暴力破解)
- **性能**:
- jsoniter 高性能JSON序列化替代标准库
- PostgreSQL索引优化
- Redis缓存验证码、会话等
- 预签名URL减轻服务器压力
- 连接池管理
- **可靠性**:
- 事务保证数据一致性
- 完整的错误处理和日志记录
- 优雅关闭和资源清理
- 对象存储连接失败时服务仍可启动
- **可扩展**:
- 清晰的函数式架构
- 管理器模式统一管理依赖
- 环境变量配置(便于容器化)
- **审计**:
- 登录日志(成功/失败)
- 操作审计
- 下载记录
## 开发指南
### 代码结构
- `cmd/server/` - 应用入口,初始化服务
- `internal/handler/` - HTTP请求处理
- `internal/service/` - 业务逻辑实现
- `internal/repository/` - 数据库操作
- `internal/model/` - 数据模型定义
- `internal/types/` - 请求/响应类型定义
- `internal/middleware/` - 中间件JWT、CORS、日志等
- `pkg/` - 可复用的公共库
### 开发规范
1. **代码风格**: 遵循Go官方代码规范使用 `gofmt` 格式化
2. **架构模式**: 使用函数式设计,避免不必要的结构体和方法
3. **依赖管理**: 通过管理器函数获取依赖(如 `database.MustGetDB()`),避免链式传递
4. **错误处理**: 使用统一的错误响应格式 (`model.NewErrorResponse`)
5. **日志记录**: 使用 Zap 结构化日志,通过 `logger.MustGetLogger()` 获取实例
6. **JSON序列化**: 使用 jsoniter 替代标准库 json提升性能
7. **RESTful API**: 遵循 REST 设计原则合理使用HTTP方法
### 添加新功能
1.`internal/model/` 定义数据模型GORM会自动迁移
2.`internal/repository/` 实现数据访问函数(使用 `database.MustGetDB()` 获取数据库)
3.`internal/service/` 实现业务逻辑函数(按需使用管理器获取依赖)
4.`internal/handler/` 实现HTTP处理函数使用管理器获取logger、jwtService等
5.`internal/handler/routes.go` 注册路由
**示例**:
```go
// Repository层
func FindUserByID(id uint) (*model.User, error) {
db := database.MustGetDB()
var user model.User
err := db.First(&user, id).Error
return &user, err
}
// Service层
func GetUserProfile(userID uint) (*model.User, error) {
logger := logger.MustGetLogger()
user, err := repository.FindUserByID(userID)
if err != nil {
logger.Error("获取用户失败", zap.Error(err))
return nil, err
}
return user, nil
}
// Handler层
func GetUserProfile(c *gin.Context) {
logger := logger.MustGetLogger()
jwtService := auth.MustGetJWTService()
// ... 处理逻辑
}
```
## 部署
### 本地开发
```bash ```bash
# 安装依赖 # 运行单元测试
go mod download go test ./...
# 配置环境变量(创建.env文件或直接export # 重新生成 swagger
cp .env.example .env swag init -g cmd/server/main.go -o docs
# 编辑 .env 文件
# 启动服务 # 代码格式化 / 静态检查
# 方式1: 使用启动脚本 gofmt -w .
./start.sh # Linux/Mac golangci-lint run (若已安装)
start.bat # Windows
# 方式2: 直接运行
go run cmd/server/main.go
``` ```
**首次启动**: ## 🧱 架构说明
- 会自动执行数据库迁移AutoMigrate创建所有表结构
- 如果对象存储未配置,会记录警告但服务仍可启动
- 检查日志确认所有服务初始化成功
### 生产部署 - **分层设计**Handler -> Service -> Repository -> Model层次清晰、职责单一。
- **依赖管理器**`pkg/*/manager.go` 使用 `sync.Once` 实现线程安全单例DB / Redis / Logger / Storage / Email / Auth / Config
- **Swagger 注解**:所有 Handler、模型、DTO 均补齐 `@Summary` / `@Description` / `@Success`,可直接生成 OpenAPI 文档。
- **配置优先级**`.env` -> 系统环境变量,所有配置均可通过容器注入。
- **自动任务**`internal/task` 承载后台作业,可按需扩展。
```bash ## 📝 Swagger 说明
# 构建二进制文件
go build -o carrotskin-server cmd/server/main.go
# 运行服务 - `start.sh` 会在启动前执行 `swag init -g cmd/server/main.go -o docs`
./carrotskin-server - 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步
``` - 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露
### Docker部署 ## 🤝 贡献指南
```bash 1. Fork & Clone
# 构建镜像 2. 创建特性分支:`git checkout -b feature/xxx`
docker build -t carrotskin-backend:latest . 3. 编写代码并补全测试 / Swagger 注释
4. 提交时附上变更说明
# 启动服务 ## 📄 许可证
docker-compose up -d
```
## 故障排查 该项目未附带开源许可证,默认保留所有权利。若需对外使用,请先与作者确认协议。
### 常见问题 ---
1. **数据库连接失败** 如需了解业务细节或 API 调用示例,请参考 `docs/swagger.yaml` 或运行服务后访问 Swagger UI。祝编码愉快🍀
- 检查 `.env` 中的数据库配置(`DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`
- 确认PostgreSQL服务已启动
- 验证数据库用户权限
- 确认数据库已创建:`createdb carrotskin``psql -c "CREATE DATABASE carrotskin;"`
- 检查数据库迁移日志,确认表结构创建成功
2. **Redis连接失败**
- 检查Redis服务是否运行`redis-cli ping`
- 验证 `.env` 中的Redis配置
- 确认Redis密码是否正确
- 检查防火墙规则
3. **RustFS/MinIO连接失败**
- 检查存储服务是否运行
- 验证访问密钥是否正确(`RUSTFS_ACCESS_KEY`, `RUSTFS_SECRET_KEY`
- 确认存储桶是否已创建(`RUSTFS_BUCKET_TEXTURES`, `RUSTFS_BUCKET_AVATARS`
- 检查网络连接和端口(`RUSTFS_ENDPOINT`
- **注意**: 如果对象存储连接失败,服务仍可启动,但上传功能不可用
4. **邮件发送失败**
- 检查 `EMAIL_ENABLED=true`
- 验证SMTP服务器地址和端口
- 确认邮箱用户名和密码正确
- 检查邮件服务商是否需要开启SMTP
- 查看日志获取详细错误信息
5. **验证码相关问题**
- 验证码过期10分钟有效期
- 发送过于频繁1分钟限制
- Redis存储失败检查Redis连接
- 邮件未收到(检查垃圾邮件)
6. **JWT验证失败**
- 检查 `JWT_SECRET` 是否配置
- 验证令牌是否过期默认168小时
- 确认请求头中包含 `Authorization: Bearer <token>`
- Token格式是否正确
### 调试技巧
1. **查看日志**
```bash
# 实时查看日志
tail -f logs/app.log
# 搜索错误日志
grep "ERROR" logs/app.log
```
2. **测试Redis连接**
```bash
redis-cli -h localhost -p 6379 -a your_password
> PING
> KEYS *
```
3. **测试数据库连接**
```bash
psql -h localhost -U postgres -d carrotskin
\dt # 查看所有表
```
4. **测试邮件配置**
- 使用Swagger文档测试 `/api/v1/auth/send-code` 接口
- 检查邮件服务商是否限制发送频率
### 开发调试
启用详细日志:
```bash
# 在 .env 中设置
LOG_LEVEL=debug
SERVER_MODE=debug
```

View File

@@ -21,6 +21,7 @@ import (
"carrotskin/internal/container" "carrotskin/internal/container"
"carrotskin/internal/handler" "carrotskin/internal/handler"
"carrotskin/internal/middleware" "carrotskin/internal/middleware"
"carrotskin/internal/task"
"carrotskin/pkg/auth" "carrotskin/pkg/auth"
"carrotskin/pkg/config" "carrotskin/pkg/config"
"carrotskin/pkg/database" "carrotskin/pkg/database"
@@ -70,11 +71,18 @@ func main() {
loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err)) loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err))
} }
// 初始化Redis // 初始化Redis(开发/测试环境失败时会自动回退到miniredis
if err := redis.Init(cfg.Redis, loggerInstance); err != nil { if err := redis.Init(cfg.Redis, loggerInstance); err != nil {
loggerInstance.Fatal("Redis连接失败", zap.Error(err)) loggerInstance.Fatal("Redis初始化失败", zap.Error(err))
}
defer redis.Close()
// 记录Redis模式
if redis.IsUsingMiniRedis() {
loggerInstance.Info("使用miniredis进行开发/测试")
} else {
loggerInstance.Info("使用生产Redis")
} }
defer redis.MustGetClient().Close()
// 初始化对象存储 (RustFS - S3兼容) // 初始化对象存储 (RustFS - S3兼容)
var storageClient *storage.StorageClient var storageClient *storage.StorageClient
@@ -91,12 +99,19 @@ func main() {
} }
emailServiceInstance := email.MustGetService() emailServiceInstance := email.MustGetService()
// 初始化Casbin权限服务
casbinService, err := auth.NewCasbinService(database.MustGetDB(), cfg.Casbin.ModelPath, loggerInstance)
if err != nil {
loggerInstance.Fatal("Casbin服务初始化失败", zap.Error(err))
}
// 创建依赖注入容器 // 创建依赖注入容器
c := container.NewContainer( c := container.NewContainer(
database.MustGetDB(), database.MustGetDB(),
redis.MustGetClient(), redis.MustGetClient(),
loggerInstance, loggerInstance,
auth.MustGetJWTService(), auth.MustGetJWTService(),
casbinService,
storageClient, storageClient,
emailServiceInstance, emailServiceInstance,
) )
@@ -121,6 +136,13 @@ func main() {
// 使用依赖注入方式注册路由 // 使用依赖注入方式注册路由
handler.RegisterRoutesWithDI(router, c) handler.RegisterRoutesWithDI(router, c)
// 启动后台任务Token已迁移到Redis不再需要清理任务
// 如需使用数据库Token存储可以恢复TokenCleanupTask
taskRunner := task.NewRunner(loggerInstance)
taskCtx, taskCancel := context.WithCancel(context.Background())
defer taskCancel()
taskRunner.Start(taskCtx)
// 创建HTTP服务器 // 创建HTTP服务器
srv := &http.Server{ srv := &http.Server{
Addr: cfg.Server.Port, Addr: cfg.Server.Port,
@@ -143,6 +165,10 @@ func main() {
<-quit <-quit
loggerInstance.Info("正在关闭服务器...") loggerInstance.Info("正在关闭服务器...")
// 停止后台任务
taskCancel()
taskRunner.Wait()
// 设置关闭超时 // 设置关闭超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()

View File

@@ -11,4 +11,4 @@ g = _, _
e = some(where (p.eft == allow)) e = some(where (p.eft == allow))
[matchers] [matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act m = g(r.sub, p.sub) && (p.obj == "*" || r.obj == p.obj) && (p.act == "*" || r.act == p.act)

View File

@@ -12,9 +12,21 @@ services:
ports: ports:
- "${APP_PORT:-8080}:8080" - "${APP_PORT:-8080}:8080"
environment: environment:
# 站点配置
- SITE_NAME=${SITE_NAME:-CarrotSkin}
- SITE_DESCRIPTION=${SITE_DESCRIPTION:-一个优秀的Minecraft皮肤站}
- REGISTRATION_ENABLED=${REGISTRATION_ENABLED:-true}
- DEFAULT_AVATAR=${DEFAULT_AVATAR:-}
# 用户限制配置
- MAX_TEXTURES_PER_USER=${MAX_TEXTURES_PER_USER:-50}
- MAX_PROFILES_PER_USER=${MAX_PROFILES_PER_USER:-5}
# 积分配置
- CHECKIN_REWARD=${CHECKIN_REWARD:-10}
- TEXTURE_DOWNLOAD_REWARD=${TEXTURE_DOWNLOAD_REWARD:-1}
# 服务器配置 # 服务器配置
- SERVER_PORT=:8080 - SERVER_PORT=:8080
- SERVER_MODE=${SERVER_MODE:-release} - SERVER_MODE=${SERVER_MODE:-release}
- SERVER_SWAGGER_ENABLED=${SERVER_SWAGGER_ENABLED:-true}
# 数据库配置 # 数据库配置
- DATABASE_DRIVER=postgres - DATABASE_DRIVER=postgres
- DATABASE_HOST=postgres - DATABASE_HOST=postgres

57
go.mod
View File

@@ -5,7 +5,8 @@ go 1.24.0
toolchain go1.24.2 toolchain go1.24.2
require ( require (
github.com/chai2010/webp v1.4.0 github.com/alicebob/miniredis/v2 v2.31.1
github.com/casbin/casbin/v2 v2.123.0
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
@@ -15,51 +16,70 @@ require (
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
github.com/wenlng/go-captcha-assets v1.0.7 github.com/wenlng/go-captcha-assets v1.0.7
github.com/wenlng/go-captcha/v2 v2.0.4 github.com/wenlng/go-captcha/v2 v2.0.4
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
gorm.io/datatypes v1.2.7 gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/glebarez/go-sqlite v1.20.3 // indirect
github.com/glebarez/sqlite v1.7.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.22.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.22.1 // indirect github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-yaml v1.19.0 // indirect github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/microsoft/go-mssqldb v1.7.2 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/tinylib/msgp v1.6.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
go.uber.org/mock v0.6.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/yuin/gopher-lua v1.1.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/image v0.33.0 // indirect golang.org/x/image v0.33.0 // indirect
golang.org/x/mod v0.30.0 // indirect golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.6.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/driver/sqlserver v1.6.0 // indirect
gorm.io/plugin/dbresolver v1.6.0 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.20.3 // indirect
) )
require ( require (
github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/casbin/gorm-adapter/v3 v3.39.0
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@@ -92,6 +112,7 @@ require (
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/swag v1.16.6
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect

230
go.sum
View File

@@ -1,7 +1,35 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -12,17 +40,27 @@ github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPII
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/casbin/casbin/v2 v2.123.0 h1:UkiMllBgn3MrwHGiZTDFVTV9up+W2CRLufZwKiuAmpA=
github.com/casbin/casbin/v2 v2.123.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
github.com/casbin/gorm-adapter/v3 v3.39.0 h1:k15txH6vE4796MuA+LFcU8I1vMjutklyzMXfjDz7lzo=
github.com/casbin/gorm-adapter/v3 v3.39.0/go.mod h1:kjXoK8MqA3E/CcqEF2l3SCkhJj1YiHVR6SF0LMvJoH4=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -37,35 +75,22 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -80,21 +105,36 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -103,6 +143,12 @@ github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -111,6 +157,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
@@ -120,20 +168,32 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
@@ -143,18 +203,30 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -174,6 +246,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -189,8 +263,8 @@ github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
@@ -200,10 +274,12 @@ github.com/wenlng/go-captcha-assets v1.0.7/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU4
github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vga3ME0= github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vga3ME0=
github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34= github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
@@ -213,7 +289,14 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
@@ -221,54 +304,106 @@ golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
@@ -281,5 +416,16 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/dbresolver v1.6.0 h1:XvKDeOtTn1EIX6s4SrKpEH82q0gXVemhYjbYZFGFVcw=
gorm.io/plugin/dbresolver v1.6.0/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=

View File

@@ -22,6 +22,7 @@ type Container struct {
Redis *redis.Client Redis *redis.Client
Logger *zap.Logger Logger *zap.Logger
JWT *auth.JWTService JWT *auth.JWTService
Casbin *auth.CasbinService
Storage *storage.StorageClient Storage *storage.StorageClient
CacheManager *database.CacheManager CacheManager *database.CacheManager
@@ -29,22 +30,19 @@ type Container struct {
UserRepo repository.UserRepository UserRepo repository.UserRepository
ProfileRepo repository.ProfileRepository ProfileRepo repository.ProfileRepository
TextureRepo repository.TextureRepository TextureRepo repository.TextureRepository
TokenRepo repository.TokenRepository
ClientRepo repository.ClientRepository ClientRepo repository.ClientRepository
ConfigRepo repository.SystemConfigRepository
YggdrasilRepo repository.YggdrasilRepository YggdrasilRepo repository.YggdrasilRepository
// Service层 // Service层
UserService service.UserService UserService service.UserService
ProfileService service.ProfileService ProfileService service.ProfileService
TextureService service.TextureService TextureService service.TextureService
TokenService service.TokenService TokenService service.TokenService
YggdrasilService service.YggdrasilService YggdrasilService service.YggdrasilService
VerificationService service.VerificationService VerificationService service.VerificationService
SecurityService service.SecurityService SecurityService service.SecurityService
CaptchaService service.CaptchaService CaptchaService service.CaptchaService
SignatureService *service.SignatureService SignatureService *service.SignatureService
TextureRenderService service.TextureRenderService
} }
// NewContainer 创建依赖容器 // NewContainer 创建依赖容器
@@ -53,6 +51,7 @@ func NewContainer(
redisClient *redis.Client, redisClient *redis.Client,
logger *zap.Logger, logger *zap.Logger,
jwtService *auth.JWTService, jwtService *auth.JWTService,
casbinService *auth.CasbinService,
storageClient *storage.StorageClient, storageClient *storage.StorageClient,
emailService interface{}, // 接受 email.Service 但使用 interface{} 避免循环依赖 emailService interface{}, // 接受 email.Service 但使用 interface{} 避免循环依赖
) *Container { ) *Container {
@@ -61,6 +60,14 @@ func NewContainer(
Prefix: "carrotskin:", Prefix: "carrotskin:",
Expiration: 5 * time.Minute, Expiration: 5 * time.Minute,
Enabled: true, Enabled: true,
Policy: database.CachePolicy{
UserTTL: 5 * time.Minute,
UserEmailTTL: 5 * time.Minute,
ProfileTTL: 5 * time.Minute,
ProfileListTTL: 3 * time.Minute,
TextureTTL: 5 * time.Minute,
TextureListTTL: 2 * time.Minute,
},
}) })
c := &Container{ c := &Container{
@@ -68,6 +75,7 @@ func NewContainer(
Redis: redisClient, Redis: redisClient,
Logger: logger, Logger: logger,
JWT: jwtService, JWT: jwtService,
Casbin: casbinService,
Storage: storageClient, Storage: storageClient,
CacheManager: cacheManager, CacheManager: cacheManager,
} }
@@ -76,9 +84,7 @@ func NewContainer(
c.UserRepo = repository.NewUserRepository(db) c.UserRepo = repository.NewUserRepository(db)
c.ProfileRepo = repository.NewProfileRepository(db) c.ProfileRepo = repository.NewProfileRepository(db)
c.TextureRepo = repository.NewTextureRepository(db) c.TextureRepo = repository.NewTextureRepository(db)
c.TokenRepo = repository.NewTokenRepository(db)
c.ClientRepo = repository.NewClientRepository(db) c.ClientRepo = repository.NewClientRepository(db)
c.ConfigRepo = repository.NewSystemConfigRepository(db)
c.YggdrasilRepo = repository.NewYggdrasilRepository(db) c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
// 初始化SignatureService作为依赖注入避免在容器中创建并立即调用 // 初始化SignatureService作为依赖注入避免在容器中创建并立即调用
@@ -86,10 +92,9 @@ func NewContainer(
c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger) c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger)
// 初始化Service注入缓存管理器 // 初始化Service注入缓存管理器
c.UserService = service.NewUserService(c.UserRepo, c.ConfigRepo, jwtService, redisClient, cacheManager, storageClient, logger) c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger) c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger) c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger)
c.TextureRenderService = service.NewTextureRenderService(c.TextureRepo, storageClient, cacheManager, logger)
// 获取Yggdrasil私钥并创建JWT服务TokenService需要 // 获取Yggdrasil私钥并创建JWT服务TokenService需要
// 注意这里仍然需要预先初始化因为TokenService在创建时需要YggdrasilJWT // 注意这里仍然需要预先初始化因为TokenService在创建时需要YggdrasilJWT
@@ -99,10 +104,24 @@ func NewContainer(
logger.Fatal("获取Yggdrasil私钥失败", zap.Error(err)) logger.Fatal("获取Yggdrasil私钥失败", zap.Error(err))
} }
yggdrasilJWT := auth.NewYggdrasilJWTService(privateKey, "carrotskin") yggdrasilJWT := auth.NewYggdrasilJWTService(privateKey, "carrotskin")
c.TokenService = service.NewTokenServiceJWT(c.TokenRepo, c.ClientRepo, c.ProfileRepo, yggdrasilJWT, logger)
// 创建Redis Token存储必须使用Redis包括miniredis回退
if redisClient == nil {
logger.Fatal("Redis客户端未初始化无法创建Token服务")
}
tokenStore := auth.NewTokenStoreRedis(
redisClient,
logger,
auth.WithKeyPrefix("token:"),
auth.WithDefaultTTL(24*time.Hour),
auth.WithStaleTTL(30*24*time.Hour),
auth.WithMaxTokensPerUser(10),
)
c.TokenService = service.NewTokenServiceRedis(tokenStore, c.ClientRepo, c.ProfileRepo, yggdrasilJWT, logger)
// 使用组合服务(内部包含认证、会话、序列化、证书服务) // 使用组合服务(内部包含认证、会话、序列化、证书服务)
c.YggdrasilService = service.NewYggdrasilServiceComposite(db, c.UserRepo, c.ProfileRepo, c.TokenRepo, c.YggdrasilRepo, c.SignatureService, redisClient, logger) c.YggdrasilService = service.NewYggdrasilServiceComposite(db, c.UserRepo, c.ProfileRepo, c.YggdrasilRepo, c.SignatureService, redisClient, logger, c.TokenService)
// 初始化其他服务 // 初始化其他服务
c.SecurityService = service.NewSecurityService(redisClient) c.SecurityService = service.NewSecurityService(redisClient)
@@ -186,20 +205,6 @@ func WithTextureRepo(repo repository.TextureRepository) Option {
} }
} }
// WithTokenRepo 设置令牌仓储
func WithTokenRepo(repo repository.TokenRepository) Option {
return func(c *Container) {
c.TokenRepo = repo
}
}
// WithConfigRepo 设置系统配置仓储
func WithConfigRepo(repo repository.SystemConfigRepository) Option {
return func(c *Container) {
c.ConfigRepo = repo
}
}
// WithUserService 设置用户服务 // WithUserService 设置用户服务
func WithUserService(svc service.UserService) Option { func WithUserService(svc service.UserService) Option {
return func(c *Container) { return func(c *Container) {

View File

@@ -0,0 +1,38 @@
package errors
import (
"errors"
"testing"
)
func TestAppErrorBasics(t *testing.T) {
root := errors.New("root")
appErr := NewBadRequest("bad", root)
if appErr.Code != 400 || appErr.Message != "bad" {
t.Fatalf("unexpected appErr fields: %+v", appErr)
}
if got := appErr.Error(); got != "bad: root" {
t.Fatalf("unexpected Error(): %s", got)
}
if !Is(appErr, root) {
t.Fatalf("Is should match wrapped error")
}
var target *AppError
if !As(appErr, &target) {
t.Fatalf("As should succeed")
}
}
func TestWrap(t *testing.T) {
if Wrap(nil, "msg") != nil {
t.Fatalf("Wrap nil should return nil")
}
err := errors.New("base")
wrapped := Wrap(err, "ctx")
if wrapped.Error() != "ctx: base" {
t.Fatalf("wrap message mismatch: %v", wrapped)
}
}

View File

@@ -0,0 +1,382 @@
package handler
import (
"net/http"
"strconv"
"carrotskin/internal/container"
"carrotskin/internal/model"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// AdminHandler 管理员处理器
type AdminHandler struct {
container *container.Container
}
// NewAdminHandler 创建管理员处理器
func NewAdminHandler(c *container.Container) *AdminHandler {
return &AdminHandler{container: c}
}
// SetUserRoleRequest 设置用户角色请求
type SetUserRoleRequest struct {
UserID int64 `json:"user_id" binding:"required"`
Role string `json:"role" binding:"required,oneof=user admin"`
}
// SetUserRole 设置用户角色
// @Summary 设置用户角色
// @Description 管理员设置指定用户的角色
// @Tags Admin
// @Accept json
// @Produce json
// @Param request body SetUserRoleRequest true "设置角色请求"
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Security BearerAuth
// @Router /api/v1/admin/users/role [put]
func (h *AdminHandler) SetUserRole(c *gin.Context) {
var req SetUserRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondBadRequest(c, "参数错误", err)
return
}
// 获取当前操作者ID
operatorID, _ := c.Get("user_id")
// 不能修改自己的角色
if req.UserID == operatorID.(int64) {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
model.CodeBadRequest,
"不能修改自己的角色",
nil,
))
return
}
// 检查目标用户是否存在
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
if err != nil || targetUser == nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse(
model.CodeNotFound,
"用户不存在",
nil,
))
return
}
// 更新用户角色
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
"role": req.Role,
})
if err != nil {
RespondServerError(c, "更新用户角色失败", err)
return
}
h.container.Logger.Info("管理员修改用户角色",
zap.Int64("operator_id", operatorID.(int64)),
zap.Int64("target_user_id", req.UserID),
zap.String("new_role", req.Role),
)
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"message": "用户角色更新成功",
"user_id": req.UserID,
"role": req.Role,
}))
}
// GetUserList 获取用户列表
// @Summary 获取用户列表
// @Description 管理员获取所有用户列表
// @Tags Admin
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Security BearerAuth
// @Router /api/v1/admin/users [get]
func (h *AdminHandler) GetUserList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
// 使用数据库直接查询用户列表
var users []model.User
var total int64
db := h.container.DB
db.Model(&model.User{}).Count(&total)
db.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&users)
// 构建响应(隐藏敏感信息)
userList := make([]gin.H, len(users))
for i, u := range users {
userList[i] = gin.H{
"id": u.ID,
"username": u.Username,
"email": u.Email,
"avatar": u.Avatar,
"role": u.Role,
"status": u.Status,
"points": u.Points,
"last_login_at": u.LastLoginAt,
"created_at": u.CreatedAt,
}
}
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"users": userList,
"total": total,
"page": page,
"page_size": pageSize,
}))
}
// GetUserDetail 获取用户详情
// @Summary 获取用户详情
// @Description 管理员获取指定用户的详细信息
// @Tags Admin
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Security BearerAuth
// @Router /api/v1/admin/users/{id} [get]
func (h *AdminHandler) GetUserDetail(c *gin.Context) {
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的用户ID", err)
return
}
user, err := h.container.UserRepo.FindByID(c.Request.Context(), userID)
if err != nil || user == nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse(
model.CodeNotFound,
"用户不存在",
nil,
))
return
}
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"avatar": user.Avatar,
"role": user.Role,
"status": user.Status,
"points": user.Points,
"properties": user.Properties,
"last_login_at": user.LastLoginAt,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
}))
}
// SetUserStatusRequest 设置用户状态请求
type SetUserStatusRequest struct {
UserID int64 `json:"user_id" binding:"required"`
Status int16 `json:"status" binding:"required,oneof=1 0 -1"` // 1:正常, 0:禁用, -1:删除
}
// SetUserStatus 设置用户状态
// @Summary 设置用户状态
// @Description 管理员设置用户状态(启用/禁用)
// @Tags Admin
// @Accept json
// @Produce json
// @Param request body SetUserStatusRequest true "设置状态请求"
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Security BearerAuth
// @Router /api/v1/admin/users/status [put]
func (h *AdminHandler) SetUserStatus(c *gin.Context) {
var req SetUserStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondBadRequest(c, "参数错误", err)
return
}
operatorID, _ := c.Get("user_id")
// 不能修改自己的状态
if req.UserID == operatorID.(int64) {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
model.CodeBadRequest,
"不能修改自己的状态",
nil,
))
return
}
// 检查目标用户是否存在
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
if err != nil || targetUser == nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse(
model.CodeNotFound,
"用户不存在",
nil,
))
return
}
// 更新用户状态
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
"status": req.Status,
})
if err != nil {
RespondServerError(c, "更新用户状态失败", err)
return
}
statusText := map[int16]string{1: "正常", 0: "禁用", -1: "删除"}[req.Status]
h.container.Logger.Info("管理员修改用户状态",
zap.Int64("operator_id", operatorID.(int64)),
zap.Int64("target_user_id", req.UserID),
zap.Int16("new_status", req.Status),
)
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"message": "用户状态更新成功",
"user_id": req.UserID,
"status": req.Status,
"status_text": statusText,
}))
}
// DeleteTexture 管理员删除材质
// @Summary 管理员删除材质
// @Description 管理员可以删除任意材质(用于审核不当内容)
// @Tags Admin
// @Produce json
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
// @Failure 404 {object} model.ErrorResponse "材质不存在"
// @Security BearerAuth
// @Router /api/v1/admin/textures/{id} [delete]
func (h *AdminHandler) DeleteTexture(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
operatorID, _ := c.Get("user_id")
// 检查材质是否存在
var texture model.Texture
if err := h.container.DB.First(&texture, textureID).Error; err != nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse(
model.CodeNotFound,
"材质不存在",
nil,
))
return
}
// 删除材质
if err := h.container.DB.Delete(&texture).Error; err != nil {
RespondServerError(c, "删除材质失败", err)
return
}
h.container.Logger.Info("管理员删除材质",
zap.Int64("operator_id", operatorID.(int64)),
zap.Int64("texture_id", textureID),
zap.Int64("uploader_id", texture.UploaderID),
zap.String("texture_name", texture.Name),
)
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"message": "材质删除成功",
"texture_id": textureID,
}))
}
// GetTextureList 管理员获取材质列表
// @Summary 管理员获取材质列表
// @Description 管理员获取所有材质列表(用于审核)
// @Tags Admin
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Security BearerAuth
// @Router /api/v1/admin/textures [get]
func (h *AdminHandler) GetTextureList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
var textures []model.Texture
var total int64
db := h.container.DB
db.Model(&model.Texture{}).Count(&total)
db.Preload("Uploader").Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&textures)
// 构建响应
textureList := make([]gin.H, len(textures))
for i, t := range textures {
uploaderName := ""
if t.Uploader != nil {
uploaderName = t.Uploader.Username
}
textureList[i] = gin.H{
"id": t.ID,
"name": t.Name,
"type": t.Type,
"hash": t.Hash,
"uploader_id": t.UploaderID,
"uploader_name": uploaderName,
"is_public": t.IsPublic,
"download_count": t.DownloadCount,
"created_at": t.CreatedAt,
}
}
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"textures": textureList,
"total": total,
"page": page,
"page_size": pageSize,
}))
}
// GetPermissions 获取权限列表
// @Summary 获取权限列表
// @Description 管理员获取所有Casbin权限规则
// @Tags Admin
// @Produce json
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Security BearerAuth
// @Router /api/v1/admin/permissions [get]
func (h *AdminHandler) GetPermissions(c *gin.Context) {
// 获取所有权限规则
policies, _ := h.container.Casbin.GetEnforcer().GetPolicy()
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"policies": policies,
}))
}

View File

@@ -31,7 +31,7 @@ func NewAuthHandler(c *container.Container) *AuthHandler {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body types.RegisterRequest true "注册信息" // @Param request body types.RegisterRequest true "注册信息"
// @Success 200 {object} model.Response "注册成功" // @Success 200 {object} model.Response{data=types.LoginResponse} "注册成功"
// @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/auth/register [post] // @Router /api/v1/auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) { func (h *AuthHandler) Register(c *gin.Context) {
@@ -107,7 +107,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body types.SendVerificationCodeRequest true "发送验证码请求" // @Param request body types.SendVerificationCodeRequest true "发送验证码请求"
// @Success 200 {object} model.Response "发送成功" // @Success 200 {object} model.Response{data=map[string]string} "发送成功"
// @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/auth/send-code [post] // @Router /api/v1/auth/send-code [post]
func (h *AuthHandler) SendVerificationCode(c *gin.Context) { func (h *AuthHandler) SendVerificationCode(c *gin.Context) {
@@ -137,7 +137,7 @@ func (h *AuthHandler) SendVerificationCode(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body types.ResetPasswordRequest true "重置密码请求" // @Param request body types.ResetPasswordRequest true "重置密码请求"
// @Success 200 {object} model.Response "重置成功" // @Success 200 {object} model.Response{data=map[string]string} "重置成功"
// @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/auth/reset-password [post] // @Router /api/v1/auth/reset-password [post]
func (h *AuthHandler) ResetPassword(c *gin.Context) { func (h *AuthHandler) ResetPassword(c *gin.Context) {

View File

@@ -34,7 +34,7 @@ type CaptchaVerifyRequest struct {
// @Tags captcha // @Tags captcha
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} map[string]interface{} "生成成功" // @Success 200 {object} map[string]interface{} "生成成功 {code: 200, data: {masterImage, tileImage, captchaId, y}}"
// @Failure 500 {object} map[string]interface{} "生成失败" // @Failure 500 {object} map[string]interface{} "生成失败"
// @Router /api/v1/captcha/generate [get] // @Router /api/v1/captcha/generate [get]
func (h *CaptchaHandler) Generate(c *gin.Context) { func (h *CaptchaHandler) Generate(c *gin.Context) {
@@ -66,7 +66,7 @@ func (h *CaptchaHandler) Generate(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body CaptchaVerifyRequest true "验证请求" // @Param request body CaptchaVerifyRequest true "验证请求"
// @Success 200 {object} map[string]interface{} "验证结果" // @Success 200 {object} map[string]interface{} "验证结果 {code: 200/400, msg: string}"
// @Failure 400 {object} map[string]interface{} "参数错误" // @Failure 400 {object} map[string]interface{} "参数错误"
// @Router /api/v1/captcha/verify [post] // @Router /api/v1/captcha/verify [post]
func (h *CaptchaHandler) Verify(c *gin.Context) { func (h *CaptchaHandler) Verify(c *gin.Context) {

View File

@@ -35,7 +35,16 @@ type CustomSkinAPIResponse struct {
} }
// GetPlayerInfo 获取玩家信息 // GetPlayerInfo 获取玩家信息
// GET {ROOT}/{USERNAME}.json // @Summary 获取玩家信息
// @Description CustomSkinAPI: 获取玩家皮肤配置信息
// @Tags CustomSkinAPI
// @Accept json
// @Produce json
// @Param username path string true "玩家用户名"
// @Success 200 {object} CustomSkinAPIResponse
// @Failure 400 {object} map[string]string "用户名不能为空"
// @Failure 404 {object} map[string]string "玩家未找到"
// @Router /api/v1/csl/{username} [get]
func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) { func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
username := c.Param("username") username := c.Param("username")
if username == "" { if username == "" {
@@ -136,7 +145,14 @@ func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
} }
// GetTexture 获取资源文件 // GetTexture 获取资源文件
// GET {ROOT}/textures/{hash} // @Summary 获取资源文件
// @Description CustomSkinAPI: 获取材质图片文件
// @Tags CustomSkinAPI
// @Param hash path string true "材质Hash"
// @Success 200 {file} binary
// @Failure 400 {object} map[string]string "资源标识符不能为空"
// @Failure 404 {object} map[string]string "资源未找到或不可用"
// @Router /api/v1/csl/textures/{hash} [get]
func (h *CustomSkinHandler) GetTexture(c *gin.Context) { func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
hash := c.Param("hash") hash := c.Param("hash")
if hash == "" { if hash == "" {

View File

@@ -72,7 +72,8 @@ func (h *ProfileHandler) Create(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Success 200 {object} model.Response "获取成功" // @Success 200 {object} model.Response{data=[]types.ProfileInfo} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile [get] // @Router /api/v1/profile [get]
func (h *ProfileHandler) List(c *gin.Context) { func (h *ProfileHandler) List(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
@@ -100,7 +101,7 @@ func (h *ProfileHandler) List(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param uuid path string true "档案UUID" // @Param uuid path string true "档案UUID"
// @Success 200 {object} model.Response "获取成功" // @Success 200 {object} model.Response{data=types.ProfileInfo} "获取成功"
// @Failure 404 {object} model.ErrorResponse "档案不存在" // @Failure 404 {object} model.ErrorResponse "档案不存在"
// @Router /api/v1/profile/{uuid} [get] // @Router /api/v1/profile/{uuid} [get]
func (h *ProfileHandler) Get(c *gin.Context) { func (h *ProfileHandler) Get(c *gin.Context) {
@@ -132,7 +133,7 @@ func (h *ProfileHandler) Get(c *gin.Context) {
// @Security BearerAuth // @Security BearerAuth
// @Param uuid path string true "档案UUID" // @Param uuid path string true "档案UUID"
// @Param request body types.UpdateProfileRequest true "更新信息" // @Param request body types.UpdateProfileRequest true "更新信息"
// @Success 200 {object} model.Response "更新成功" // @Success 200 {object} model.Response{data=types.ProfileInfo} "更新成功"
// @Failure 403 {object} model.ErrorResponse "无权操作" // @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/profile/{uuid} [put] // @Router /api/v1/profile/{uuid} [put]
func (h *ProfileHandler) Update(c *gin.Context) { func (h *ProfileHandler) Update(c *gin.Context) {
@@ -180,7 +181,7 @@ func (h *ProfileHandler) Update(c *gin.Context) {
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Param uuid path string true "档案UUID" // @Param uuid path string true "档案UUID"
// @Success 200 {object} model.Response "删除成功" // @Success 200 {object} model.Response{data=map[string]string} "删除成功"
// @Failure 403 {object} model.ErrorResponse "无权操作" // @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/profile/{uuid} [delete] // @Router /api/v1/profile/{uuid} [delete]
func (h *ProfileHandler) Delete(c *gin.Context) { func (h *ProfileHandler) Delete(c *gin.Context) {

View File

@@ -3,8 +3,8 @@ package handler
import ( import (
"carrotskin/internal/container" "carrotskin/internal/container"
"carrotskin/internal/middleware" "carrotskin/internal/middleware"
"carrotskin/internal/model"
"carrotskin/pkg/auth" "carrotskin/pkg/auth"
"carrotskin/pkg/config"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
@@ -20,6 +20,7 @@ type Handlers struct {
Captcha *CaptchaHandler Captcha *CaptchaHandler
Yggdrasil *YggdrasilHandler Yggdrasil *YggdrasilHandler
CustomSkin *CustomSkinHandler CustomSkin *CustomSkinHandler
Admin *AdminHandler
} }
// NewHandlers 创建所有Handler实例 // NewHandlers 创建所有Handler实例
@@ -32,6 +33,7 @@ func NewHandlers(c *container.Container) *Handlers {
Captcha: NewCaptchaHandler(c), Captcha: NewCaptchaHandler(c),
Yggdrasil: NewYggdrasilHandler(c), Yggdrasil: NewYggdrasilHandler(c),
CustomSkin: NewCustomSkinHandler(c), CustomSkin: NewCustomSkinHandler(c),
Admin: NewAdminHandler(c),
} }
} }
@@ -41,7 +43,10 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
router.GET("/health", HealthCheck) router.GET("/health", HealthCheck)
// Swagger文档路由 // Swagger文档路由
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) cfg, _ := config.GetConfig()
if cfg != nil && cfg.Server.SwaggerEnabled {
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
// 创建Handler实例 // 创建Handler实例
h := NewHandlers(c) h := NewHandlers(c)
@@ -67,11 +72,11 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
// Yggdrasil API路由组 // Yggdrasil API路由组
registerYggdrasilRoutesWithDI(v1, h.Yggdrasil) registerYggdrasilRoutesWithDI(v1, h.Yggdrasil)
// 系统路由
registerSystemRoutes(v1)
// CustomSkinAPI 路由 // CustomSkinAPI 路由
registerCustomSkinRoutes(v1, h.CustomSkin) registerCustomSkinRoutes(v1, h.CustomSkin)
// 管理员路由(需要管理员权限)
registerAdminRoutes(v1, c, h.Admin)
} }
} }
@@ -113,10 +118,6 @@ func registerTextureRoutes(v1 *gin.RouterGroup, h *TextureHandler, jwtService *a
// 公开路由(无需认证) // 公开路由(无需认证)
textureGroup.GET("", h.Search) textureGroup.GET("", h.Search)
textureGroup.GET("/:id", h.Get) textureGroup.GET("/:id", h.Get)
textureGroup.GET("/:id/render", h.RenderTexture) // type/front/back/full/head/isometric
textureGroup.GET("/:id/avatar", h.RenderAvatar) // mode=2d/3d
textureGroup.GET("/:id/cape", h.RenderCape)
textureGroup.GET("/:id/preview", h.RenderPreview) // 自动根据类型预览
// 需要认证的路由 // 需要认证的路由
textureAuth := textureGroup.Group("") textureAuth := textureGroup.Group("")
@@ -191,20 +192,25 @@ func registerYggdrasilRoutesWithDI(v1 *gin.RouterGroup, h *YggdrasilHandler) {
} }
} }
// registerSystemRoutes 注册系统路由 // registerAdminRoutes 注册管理员路由
func registerSystemRoutes(v1 *gin.RouterGroup) { func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHandler) {
system := v1.Group("/system") admin := v1.Group("/admin")
admin.Use(middleware.AuthMiddleware(c.JWT))
admin.Use(middleware.RequireAdmin())
{ {
system.GET("/config", func(c *gin.Context) {
// TODO: 实现从数据库读取系统配置 // 用户管理
c.JSON(200, model.NewSuccessResponse(gin.H{ admin.GET("/users", h.GetUserList)
"site_name": "CarrotSkin", admin.GET("/users/:id", h.GetUserDetail)
"site_description": "A Minecraft Skin Station", admin.PUT("/users/role", h.SetUserRole)
"registration_enabled": true, admin.PUT("/users/status", h.SetUserStatus)
"max_textures_per_user": 100,
"max_profiles_per_user": 5, // 材质管理(审核)
})) admin.GET("/textures", h.GetTextureList)
}) admin.DELETE("/textures/:id", h.DeleteTexture)
// 权限管理
admin.GET("/permissions", h.GetPermissions)
} }
} }

View File

@@ -0,0 +1,27 @@
package handler
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
// 仅验证降级路径(未初始化依赖时的响应)
func TestHealthCheck_Degraded(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/health", HealthCheck)
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503 when dependencies missing, got %d", w.Code)
}
}

View File

@@ -3,7 +3,6 @@ package handler
import ( import (
"carrotskin/internal/container" "carrotskin/internal/container"
"carrotskin/internal/model" "carrotskin/internal/model"
"carrotskin/internal/service"
"carrotskin/internal/types" "carrotskin/internal/types"
"strconv" "strconv"
@@ -26,6 +25,16 @@ func NewTextureHandler(c *container.Container) *TextureHandler {
} }
// Get 获取材质详情 // Get 获取材质详情
// @Summary 获取材质详情
// @Description 获取指定ID的材质详细信息
// @Tags texture
// @Accept json
// @Produce json
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response{data=types.TextureInfo} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "材质不存在"
// @Router /api/v1/texture/{id} [get]
func (h *TextureHandler) Get(c *gin.Context) { func (h *TextureHandler) Get(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64) id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil { if err != nil {
@@ -43,6 +52,19 @@ func (h *TextureHandler) Get(c *gin.Context) {
} }
// Search 搜索材质 // Search 搜索材质
// @Summary 搜索材质
// @Description 搜索材质列表,支持关键词、类型、公开性筛选和分页
// @Tags texture
// @Accept json
// @Produce json
// @Param keyword query string false "关键词"
// @Param type query string false "材质类型 (SKIN/CAPE)"
// @Param public_only query boolean false "仅显示公开材质"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/texture [get]
func (h *TextureHandler) Search(c *gin.Context) { func (h *TextureHandler) Search(c *gin.Context) {
keyword := c.Query("keyword") keyword := c.Query("keyword")
textureTypeStr := c.Query("type") textureTypeStr := c.Query("type")
@@ -85,99 +107,19 @@ func (h *TextureHandler) Search(c *gin.Context) {
}) })
} }
// RenderTexture 渲染皮肤/披风预览
func (h *TextureHandler) RenderTexture(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
renderType := service.RenderType(c.DefaultQuery("type", string(service.RenderTypeIsometric)))
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderTexture(c.Request.Context(), textureID, renderType, size, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// RenderAvatar 渲染头像2D/3D
func (h *TextureHandler) RenderAvatar(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
mode := service.AvatarMode(c.DefaultQuery("mode", string(service.AvatarMode2D)))
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderAvatar(c.Request.Context(), textureID, size, mode, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// RenderCape 渲染披风
func (h *TextureHandler) RenderCape(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderCape(c.Request.Context(), textureID, size, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// RenderPreview 自动选择预览(皮肤走等距,披风走披风渲染)
func (h *TextureHandler) RenderPreview(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderPreview(c.Request.Context(), textureID, size, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// toRenderResponse 转换为API响应
func toRenderResponse(r *service.RenderResult) *types.RenderResponse {
if r == nil {
return nil
}
resp := &types.RenderResponse{
URL: r.URL,
ContentType: r.ContentType,
ETag: r.ETag,
Size: r.Size,
}
if !r.LastModified.IsZero() {
t := r.LastModified
resp.LastModified = &t
}
return resp
}
// Update 更新材质 // Update 更新材质
// @Summary 更新材质
// @Description 更新材质信息(名称、描述、公开性)
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "材质ID"
// @Param request body types.UpdateTextureRequest true "更新信息"
// @Success 200 {object} model.Response{data=types.TextureInfo} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/texture/{id} [put]
func (h *TextureHandler) Update(c *gin.Context) { func (h *TextureHandler) Update(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -211,6 +153,17 @@ func (h *TextureHandler) Update(c *gin.Context) {
} }
// Delete 删除材质 // Delete 删除材质
// @Summary 删除材质
// @Description 删除指定ID的材质
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response "删除成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/texture/{id} [delete]
func (h *TextureHandler) Delete(c *gin.Context) { func (h *TextureHandler) Delete(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -237,6 +190,16 @@ func (h *TextureHandler) Delete(c *gin.Context) {
} }
// ToggleFavorite 切换收藏状态 // ToggleFavorite 切换收藏状态
// @Summary 切换收藏状态
// @Description 收藏或取消收藏指定材质
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response{data=map[string]bool} "操作成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Router /api/v1/texture/{id} [post]
func (h *TextureHandler) ToggleFavorite(c *gin.Context) { func (h *TextureHandler) ToggleFavorite(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -264,6 +227,17 @@ func (h *TextureHandler) ToggleFavorite(c *gin.Context) {
} }
// GetUserTextures 获取用户上传的材质列表 // GetUserTextures 获取用户上传的材质列表
// @Summary 获取我的材质
// @Description 获取当前登录用户上传的材质列表
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/texture/my [get]
func (h *TextureHandler) GetUserTextures(c *gin.Context) { func (h *TextureHandler) GetUserTextures(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -289,6 +263,17 @@ func (h *TextureHandler) GetUserTextures(c *gin.Context) {
} }
// GetUserFavorites 获取用户收藏的材质列表 // GetUserFavorites 获取用户收藏的材质列表
// @Summary 获取我的收藏
// @Description 获取当前登录用户收藏的材质列表
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/texture/favorites [get]
func (h *TextureHandler) GetUserFavorites(c *gin.Context) { func (h *TextureHandler) GetUserFavorites(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -314,6 +299,21 @@ func (h *TextureHandler) GetUserFavorites(c *gin.Context) {
} }
// Upload 直接上传材质文件 // Upload 直接上传材质文件
// @Summary 上传材质
// @Description 上传图片文件创建新材质
// @Tags texture
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param file formData file true "材质文件 (PNG)"
// @Param name formData string true "材质名称"
// @Param description formData string false "材质描述"
// @Param type formData string false "材质类型 (SKIN/CAPE)" default(SKIN)
// @Param is_public formData boolean false "是否公开" default(false)
// @Param is_slim formData boolean false "是否为纤细模型 (仅SKIN有效)" default(false)
// @Success 200 {object} model.Response{data=types.TextureInfo} "上传成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Router /api/v1/texture/upload [post]
func (h *TextureHandler) Upload(c *gin.Context) { func (h *TextureHandler) Upload(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {

View File

@@ -24,6 +24,15 @@ func NewUserHandler(c *container.Container) *UserHandler {
} }
// GetProfile 获取用户信息 // GetProfile 获取用户信息
// @Summary 获取用户信息
// @Description 获取当前登录用户的详细信息
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} model.Response{data=types.UserInfo} "获取成功"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/profile [get]
func (h *UserHandler) GetProfile(c *gin.Context) { func (h *UserHandler) GetProfile(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -44,6 +53,17 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
} }
// UpdateProfile 更新用户信息 // UpdateProfile 更新用户信息
// @Summary 更新用户信息
// @Description 更新用户资料密码、头像URL如需上传头像请使用上传接口
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body types.UpdateUserRequest true "更新信息"
// @Success 200 {object} model.Response{data=types.UserInfo} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/profile [put]
func (h *UserHandler) UpdateProfile(c *gin.Context) { func (h *UserHandler) UpdateProfile(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -103,6 +123,17 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
} }
// UploadAvatar 直接上传头像文件 // UploadAvatar 直接上传头像文件
// @Summary 上传头像
// @Description 上传图片文件作为用户头像
// @Tags user
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param file formData file true "头像文件"
// @Success 200 {object} model.Response{data=map[string]interface{}} "上传成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/avatar/upload [post]
func (h *UserHandler) UploadAvatar(c *gin.Context) { func (h *UserHandler) UploadAvatar(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -162,6 +193,17 @@ func (h *UserHandler) UploadAvatar(c *gin.Context) {
} }
// UpdateAvatar 更新头像URL保留用于外部URL // UpdateAvatar 更新头像URL保留用于外部URL
// @Summary 更新头像URL
// @Description 更新用户头像为外部URL
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param avatar_url query string true "头像URL"
// @Success 200 {object} model.Response{data=types.UserInfo} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/avatar [put]
func (h *UserHandler) UpdateAvatar(c *gin.Context) { func (h *UserHandler) UpdateAvatar(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -199,6 +241,17 @@ func (h *UserHandler) UpdateAvatar(c *gin.Context) {
} }
// ChangeEmail 更换邮箱 // ChangeEmail 更换邮箱
// @Summary 更换邮箱
// @Description 更换用户绑定的邮箱,需要验证码
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body types.ChangeEmailRequest true "更换邮箱请求"
// @Success 200 {object} model.Response{data=types.UserInfo} "更换成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/change-email [post]
func (h *UserHandler) ChangeEmail(c *gin.Context) { func (h *UserHandler) ChangeEmail(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {
@@ -237,6 +290,15 @@ func (h *UserHandler) ChangeEmail(c *gin.Context) {
} }
// ResetYggdrasilPassword 重置Yggdrasil密码 // ResetYggdrasilPassword 重置Yggdrasil密码
// @Summary 重置Yggdrasil密码
// @Description 重置用户的Yggdrasil API认证密码
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} model.Response{data=map[string]string} "重置成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/user/yggdrasil-password/reset [post]
func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) { func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) {
userID, ok := GetUserIDFromContext(c) userID, ok := GetUserIDFromContext(c)
if !ok { if !ok {

View File

@@ -167,6 +167,15 @@ func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler {
} }
// Authenticate 用户认证 // Authenticate 用户认证
// @Summary Yggdrasil认证
// @Description Yggdrasil协议: 用户登录认证
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body AuthenticateRequest true "认证请求"
// @Success 200 {object} AuthenticateResponse
// @Failure 403 {object} map[string]string "认证失败"
// @Router /api/v1/yggdrasil/authserver/authenticate [post]
func (h *YggdrasilHandler) Authenticate(c *gin.Context) { func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
rawData, err := io.ReadAll(c.Request.Body) rawData, err := io.ReadAll(c.Request.Body)
if err != nil { if err != nil {
@@ -248,6 +257,15 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
} }
// ValidToken 验证令牌 // ValidToken 验证令牌
// @Summary Yggdrasil验证令牌
// @Description Yggdrasil协议: 验证AccessToken是否有效
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body ValidTokenRequest true "验证请求"
// @Success 204 "令牌有效"
// @Failure 403 {object} map[string]bool "令牌无效"
// @Router /api/v1/yggdrasil/authserver/validate [post]
func (h *YggdrasilHandler) ValidToken(c *gin.Context) { func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
var request ValidTokenRequest var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil { if err := c.ShouldBindJSON(&request); err != nil {
@@ -266,6 +284,15 @@ func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
} }
// RefreshToken 刷新令牌 // RefreshToken 刷新令牌
// @Summary Yggdrasil刷新令牌
// @Description Yggdrasil协议: 刷新AccessToken
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body RefreshRequest true "刷新请求"
// @Success 200 {object} RefreshResponse
// @Failure 400 {object} map[string]string "刷新失败"
// @Router /api/v1/yggdrasil/authserver/refresh [post]
func (h *YggdrasilHandler) RefreshToken(c *gin.Context) { func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
var request RefreshRequest var request RefreshRequest
if err := c.ShouldBindJSON(&request); err != nil { if err := c.ShouldBindJSON(&request); err != nil {
@@ -350,6 +377,14 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
} }
// InvalidToken 使令牌失效 // InvalidToken 使令牌失效
// @Summary Yggdrasil注销令牌
// @Description Yggdrasil协议: 使AccessToken失效
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body ValidTokenRequest true "失效请求"
// @Success 204 "操作成功"
// @Router /api/v1/yggdrasil/authserver/invalidate [post]
func (h *YggdrasilHandler) InvalidToken(c *gin.Context) { func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
var request ValidTokenRequest var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil { if err := c.ShouldBindJSON(&request); err != nil {
@@ -364,6 +399,15 @@ func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
} }
// SignOut 用户登出 // SignOut 用户登出
// @Summary Yggdrasil登出
// @Description Yggdrasil协议: 用户登出,使所有令牌失效
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body SignOutRequest true "登出请求"
// @Success 204 "操作成功"
// @Failure 400 {object} map[string]string "参数错误"
// @Router /api/v1/yggdrasil/authserver/signout [post]
func (h *YggdrasilHandler) SignOut(c *gin.Context) { func (h *YggdrasilHandler) SignOut(c *gin.Context) {
var request SignOutRequest var request SignOutRequest
if err := c.ShouldBindJSON(&request); err != nil { if err := c.ShouldBindJSON(&request); err != nil {
@@ -397,6 +441,15 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
} }
// GetProfileByUUID 根据UUID获取档案 // GetProfileByUUID 根据UUID获取档案
// @Summary Yggdrasil获取档案
// @Description Yggdrasil协议: 根据UUID获取用户档案信息
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param uuid path string true "用户UUID"
// @Success 200 {object} map[string]interface{} "档案信息"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid} [get]
func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) { func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
uuid := utils.FormatUUID(c.Param("uuid")) uuid := utils.FormatUUID(c.Param("uuid"))
h.logger.Info("获取配置文件请求", zap.String("uuid", uuid)) h.logger.Info("获取配置文件请求", zap.String("uuid", uuid))
@@ -413,6 +466,16 @@ func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
} }
// JoinServer 加入服务器 // JoinServer 加入服务器
// @Summary Yggdrasil加入服务器
// @Description Yggdrasil协议: 客户端加入服务器
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body JoinServerRequest true "加入请求"
// @Success 204 "加入成功"
// @Failure 400 {object} APIResponse "参数错误"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/join [post]
func (h *YggdrasilHandler) JoinServer(c *gin.Context) { func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
var request JoinServerRequest var request JoinServerRequest
clientIP := c.ClientIP() clientIP := c.ClientIP()
@@ -449,6 +512,17 @@ func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
} }
// HasJoinedServer 验证玩家是否已加入服务器 // HasJoinedServer 验证玩家是否已加入服务器
// @Summary Yggdrasil验证加入
// @Description Yggdrasil协议: 服务端验证客户端是否已加入
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param username query string true "用户名"
// @Param serverId query string true "服务器ID"
// @Param ip query string false "客户端IP"
// @Success 200 {object} map[string]interface{} "验证成功,返回档案"
// @Failure 204 "验证失败"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/hasJoined [get]
func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) { func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
clientIP, _ := c.GetQuery("ip") clientIP, _ := c.GetQuery("ip")
@@ -499,6 +573,15 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
} }
// GetProfilesByName 批量获取配置文件 // GetProfilesByName 批量获取配置文件
// @Summary Yggdrasil批量获取档案
// @Description Yggdrasil协议: 根据名称批量获取用户档案
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body []string true "用户名列表"
// @Success 200 {array} model.Profile "档案列表"
// @Failure 400 {object} APIResponse "参数错误"
// @Router /api/v1/yggdrasil/api/profiles/minecraft [post]
func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) { func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
var names []string var names []string
@@ -520,6 +603,14 @@ func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
} }
// GetMetaData 获取Yggdrasil元数据 // GetMetaData 获取Yggdrasil元数据
// @Summary Yggdrasil元数据
// @Description Yggdrasil协议: 获取服务器元数据
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{} "元数据"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil [get]
func (h *YggdrasilHandler) GetMetaData(c *gin.Context) { func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
meta := gin.H{ meta := gin.H{
"implementationName": "CellAuth", "implementationName": "CellAuth",
@@ -550,6 +641,16 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
} }
// GetPlayerCertificates 获取玩家证书 // GetPlayerCertificates 获取玩家证书
// @Summary Yggdrasil获取证书
// @Description Yggdrasil协议: 获取玩家证书
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer {token}"
// @Success 200 {object} map[string]interface{} "证书信息"
// @Failure 401 {object} map[string]string "未授权"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/minecraftservices/player/certificates [post]
func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) { func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
if authHeader == "" { if authHeader == "" {

View File

@@ -6,7 +6,7 @@ import (
"strings" "strings"
"carrotskin/pkg/auth" "carrotskin/pkg/auth"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -51,7 +51,7 @@ func AuthMiddleware(jwtService *auth.JWTService) gin.HandlerFunc {
// 将用户信息存储到上下文中 // 将用户信息存储到上下文中
c.Set("user_id", claims.UserID) c.Set("user_id", claims.UserID)
c.Set("username", claims.Username) c.Set("username", claims.Username)
c.Set("role", claims.Role) c.Set("user_role", claims.Role)
c.Next() c.Next()
}) })
@@ -69,7 +69,7 @@ func OptionalAuthMiddleware(jwtService *auth.JWTService) gin.HandlerFunc {
if err == nil { if err == nil {
c.Set("user_id", claims.UserID) c.Set("user_id", claims.UserID)
c.Set("username", claims.Username) c.Set("username", claims.Username)
c.Set("role", claims.Role) c.Set("user_role", claims.Role)
} }
} }
} }

View File

@@ -0,0 +1,109 @@
package middleware
import (
"net/http"
"carrotskin/pkg/auth"
"github.com/gin-gonic/gin"
)
// CasbinMiddleware Casbin权限中间件
// 需要先经过AuthMiddleware获取用户信息
func CasbinMiddleware(casbinService *auth.CasbinService, resource, action string) gin.HandlerFunc {
return func(c *gin.Context) {
// 从上下文获取用户角色由AuthMiddleware设置
role, exists := c.Get("user_role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未授权访问",
})
c.Abort()
return
}
roleStr, ok := role.(string)
if !ok || roleStr == "" {
roleStr = "user" // 默认角色
}
// 检查权限
if !casbinService.CheckPermission(roleStr, resource, action) {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "权限不足",
})
c.Abort()
return
}
c.Next()
}
}
// RequireAdmin 要求管理员权限的中间件
func RequireAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
role, exists := c.Get("user_role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未授权访问",
})
c.Abort()
return
}
roleStr, ok := role.(string)
if !ok || roleStr != "admin" {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "需要管理员权限",
})
c.Abort()
return
}
c.Next()
}
}
// RequireRole 要求指定角色的中间件
func RequireRole(allowedRoles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
role, exists := c.Get("user_role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未授权访问",
})
c.Abort()
return
}
roleStr, ok := role.(string)
if !ok {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "权限不足",
})
c.Abort()
return
}
// 检查是否在允许的角色列表中
for _, allowed := range allowedRoles {
if roleStr == allowed {
c.Next()
return
}
}
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "权限不足",
})
c.Abort()
}
}

View File

@@ -5,6 +5,7 @@ import (
) )
// AuditLog 审计日志模型 // AuditLog 审计日志模型
// @Description 系统操作审计日志记录
type AuditLog struct { type AuditLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_audit_logs_user_created,priority:1" json:"user_id,omitempty"` UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_audit_logs_user_created,priority:1" json:"user_id,omitempty"`
@@ -27,6 +28,7 @@ func (AuditLog) TableName() string {
} }
// CasbinRule Casbin 权限规则模型 // CasbinRule Casbin 权限规则模型
// @Description Casbin权限控制规则数据
type CasbinRule struct { type CasbinRule struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
PType string `gorm:"column:ptype;type:varchar(100);not null;index:idx_casbin_ptype;uniqueIndex:uk_casbin_rule,priority:1" json:"ptype"` PType string `gorm:"column:ptype;type:varchar(100);not null;index:idx_casbin_ptype;uniqueIndex:uk_casbin_rule,priority:1" json:"ptype"`

View File

@@ -7,6 +7,7 @@ import (
) )
// BaseModel 基础模型 // BaseModel 基础模型
// @Description 通用基础模型包含ID和时间戳字段
// 包含 uint 类型的 ID 和标准时间字段,但时间字段不通过 JSON 返回给前端 // 包含 uint 类型的 ID 和标准时间字段,但时间字段不通过 JSON 返回给前端
type BaseModel struct { type BaseModel struct {
// ID 主键 // ID 主键
@@ -21,11 +22,3 @@ type BaseModel struct {
// DeletedAt 删除时间 (软删除,不返回给前端) // DeletedAt 删除时间 (软删除,不返回给前端)
DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
} }

View File

@@ -3,12 +3,13 @@ package model
import "time" import "time"
// Client 客户端实体用于管理Token版本 // Client 客户端实体用于管理Token版本
// @Description Yggdrasil客户端Token管理数据
type Client struct { type Client struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` // Client UUID UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` // Client UUID
ClientToken string `gorm:"column:client_token;type:varchar(64);not null;uniqueIndex" json:"client_token"` // 客户端Token ClientToken string `gorm:"column:client_token;type:varchar(64);not null;uniqueIndex" json:"client_token"` // 客户端Token
UserID int64 `gorm:"column:user_id;not null;index:idx_clients_user_id" json:"user_id"` // 用户ID UserID int64 `gorm:"column:user_id;not null;index:idx_clients_user_id" json:"user_id"` // 用户ID
ProfileID string `gorm:"column:profile_id;type:varchar(36);index:idx_clients_profile_id" json:"profile_id,omitempty"` // 选中的Profile ProfileID string `gorm:"column:profile_id;type:varchar(36);index:idx_clients_profile_id" json:"profile_id,omitempty"` // 选中的Profile
Version int `gorm:"column:version;not null;default:0;index:idx_clients_version" json:"version"` // 版本号 Version int `gorm:"column:version;not null;default:0;index:idx_clients_version" json:"version"` // 版本号
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
@@ -21,11 +22,3 @@ type Client struct {
func (Client) TableName() string { func (Client) TableName() string {
return "clients" return "clients"
} }

View File

@@ -5,6 +5,7 @@ import (
) )
// Profile Minecraft 档案模型 // Profile Minecraft 档案模型
// @Description Minecraft角色档案数据模型
type Profile struct { type Profile struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"`
UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1" json:"user_id"` UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1" json:"user_id"`
@@ -28,6 +29,7 @@ func (Profile) TableName() string {
} }
// ProfileResponse 档案响应(包含完整的皮肤/披风信息) // ProfileResponse 档案响应(包含完整的皮肤/披风信息)
// @Description Minecraft档案完整响应数据
type ProfileResponse struct { type ProfileResponse struct {
UUID string `json:"uuid"` UUID string `json:"uuid"`
Name string `json:"name"` Name string `json:"name"`
@@ -37,22 +39,27 @@ type ProfileResponse struct {
} }
// ProfileTexturesData Minecraft 材质数据结构 // ProfileTexturesData Minecraft 材质数据结构
// @Description Minecraft档案材质数据
type ProfileTexturesData struct { type ProfileTexturesData struct {
Skin *ProfileTexture `json:"SKIN,omitempty"` Skin *ProfileTexture `json:"SKIN,omitempty"`
Cape *ProfileTexture `json:"CAPE,omitempty"` Cape *ProfileTexture `json:"CAPE,omitempty"`
} }
// ProfileTexture 单个材质信息 // ProfileTexture 单个材质信息
// @Description 单个材质的详细信息
type ProfileTexture struct { type ProfileTexture struct {
URL string `json:"url"` URL string `json:"url"`
Metadata *ProfileTextureMetadata `json:"metadata,omitempty"` Metadata *ProfileTextureMetadata `json:"metadata,omitempty"`
} }
// ProfileTextureMetadata 材质元数据 // ProfileTextureMetadata 材质元数据
// @Description 材质的元数据信息
type ProfileTextureMetadata struct { type ProfileTextureMetadata struct {
Model string `json:"model,omitempty"` // "slim" or "classic" Model string `json:"model,omitempty"` // "slim" or "classic"
} }
// KeyPair RSA密钥对
// @Description 用于Yggdrasil认证的RSA密钥对
type KeyPair struct { type KeyPair struct {
PrivateKey string `json:"private_key" bson:"private_key"` PrivateKey string `json:"private_key" bson:"private_key"`
PublicKey string `json:"public_key" bson:"public_key"` PublicKey string `json:"public_key" bson:"public_key"`

View File

@@ -3,6 +3,7 @@ package model
import "os" import "os"
// Response 通用API响应结构 // Response 通用API响应结构
// @Description 标准API响应格式
type Response struct { type Response struct {
Code int `json:"code"` // 业务状态码 Code int `json:"code"` // 业务状态码
Message string `json:"message"` // 响应消息 Message string `json:"message"` // 响应消息
@@ -10,6 +11,7 @@ type Response struct {
} }
// PaginationResponse 分页响应结构 // PaginationResponse 分页响应结构
// @Description 分页数据响应格式
type PaginationResponse struct { type PaginationResponse struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` Message string `json:"message"`
@@ -20,6 +22,7 @@ type PaginationResponse struct {
} }
// ErrorResponse 错误响应 // ErrorResponse 错误响应
// @Description API错误响应格式
type ErrorResponse struct { type ErrorResponse struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` Message string `json:"message"`

View File

@@ -1,41 +0,0 @@
package model
import (
"time"
)
// ConfigType 配置类型
type ConfigType string
const (
ConfigTypeString ConfigType = "STRING"
ConfigTypeInteger ConfigType = "INTEGER"
ConfigTypeBoolean ConfigType = "BOOLEAN"
ConfigTypeJSON ConfigType = "JSON"
)
// SystemConfig 系统配置模型
type SystemConfig struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Key string `gorm:"column:key;type:varchar(100);not null;uniqueIndex" json:"key"`
Value string `gorm:"column:value;type:text;not null" json:"value"`
Description string `gorm:"column:description;type:varchar(255);not null;default:''" json:"description"`
Type ConfigType `gorm:"column:type;type:varchar(50);not null;default:'STRING'" json:"type"` // STRING, INTEGER, BOOLEAN, JSON
IsPublic bool `gorm:"column:is_public;not null;default:false;index" json:"is_public"` // 是否可被前端获取
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
}
// TableName 指定表名
func (SystemConfig) TableName() string {
return "system_config"
}
// SystemConfigPublicResponse 公开配置响应
type SystemConfigPublicResponse struct {
SiteName string `json:"site_name"`
SiteDescription string `json:"site_description"`
RegistrationEnabled bool `json:"registration_enabled"`
MaintenanceMode bool `json:"maintenance_mode"`
Announcement string `json:"announcement"`
}

View File

@@ -5,6 +5,7 @@ import (
) )
// TextureType 材质类型 // TextureType 材质类型
// @Description 材质类型枚举SKIN(皮肤)或CAPE(披风)
type TextureType string type TextureType string
const ( const (
@@ -13,6 +14,7 @@ const (
) )
// Texture 材质模型 // Texture 材质模型
// @Description Minecraft材质数据模型
type Texture struct { type Texture struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UploaderID int64 `gorm:"column:uploader_id;not null;index:idx_textures_uploader_status,priority:1;index:idx_textures_uploader_created,priority:1" json:"uploader_id"` UploaderID int64 `gorm:"column:uploader_id;not null;index:idx_textures_uploader_status,priority:1;index:idx_textures_uploader_created,priority:1" json:"uploader_id"`
@@ -40,6 +42,7 @@ func (Texture) TableName() string {
} }
// UserTextureFavorite 用户材质收藏 // UserTextureFavorite 用户材质收藏
// @Description 用户收藏材质关联表
type UserTextureFavorite struct { type UserTextureFavorite struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_texture,priority:1;index:idx_favorites_user_created,priority:1" json:"user_id"` UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_texture,priority:1;index:idx_favorites_user_created,priority:1" json:"user_id"`
@@ -57,6 +60,7 @@ func (UserTextureFavorite) TableName() string {
} }
// TextureDownloadLog 材质下载记录 // TextureDownloadLog 材质下载记录
// @Description 材质下载日志记录
type TextureDownloadLog struct { type TextureDownloadLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
TextureID int64 `gorm:"column:texture_id;not null;index:idx_download_logs_texture_created,priority:1" json:"texture_id"` TextureID int64 `gorm:"column:texture_id;not null;index:idx_download_logs_texture_created,priority:1" json:"texture_id"`

View File

@@ -1,23 +0,0 @@
package model
import "time"
// Token Yggdrasil 认证令牌模型
type Token struct {
AccessToken string `gorm:"column:access_token;type:text;primaryKey" json:"access_token"` // 改为text以支持JWT长度
UserID int64 `gorm:"column:user_id;not null;index:idx_tokens_user_id" json:"user_id"`
ClientToken string `gorm:"column:client_token;type:varchar(64);not null;index:idx_tokens_client_token" json:"client_token"`
ProfileId string `gorm:"column:profile_id;type:varchar(36);index:idx_tokens_profile_id" json:"profile_id"` // 改为可空
Version int `gorm:"column:version;not null;default:0;index:idx_tokens_version" json:"version"` // 新增:版本号
Usable bool `gorm:"column:usable;not null;default:true;index:idx_tokens_usable" json:"usable"`
IssueDate time.Time `gorm:"column:issue_date;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_tokens_issue_date,sort:desc" json:"issue_date"`
ExpiresAt *time.Time `gorm:"column:expires_at;type:timestamp" json:"expires_at,omitempty"` // 新增:过期时间
StaleAt *time.Time `gorm:"column:stale_at;type:timestamp" json:"stale_at,omitempty"` // 新增:过期但可用时间
// 关联
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"user,omitempty"`
Profile *Profile `gorm:"foreignKey:ProfileId;references:UUID;constraint:OnDelete:CASCADE" json:"profile,omitempty"`
}
// TableName 指定表名
func (Token) TableName() string { return "tokens" }

View File

@@ -7,6 +7,7 @@ import (
) )
// User 用户模型 // User 用户模型
// @Description 用户账户数据模型
type User struct { type User struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex:idx_user_username_status,priority:1" json:"username"` Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex:idx_user_username_status,priority:1" json:"username"`
@@ -16,7 +17,7 @@ type User struct {
Points int `gorm:"column:points;type:integer;not null;default:0;index:idx_user_points,sort:desc" json:"points"` Points int `gorm:"column:points;type:integer;not null;default:0;index:idx_user_points,sort:desc" json:"points"`
Role string `gorm:"column:role;type:varchar(50);not null;default:'user';index:idx_user_role_status,priority:1" json:"role"` Role string `gorm:"column:role;type:varchar(50);not null;default:'user';index:idx_user_role_status,priority:1" json:"role"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_user_username_status,priority:2;index:idx_user_email_status,priority:2;index:idx_user_role_status,priority:2" json:"status"` // 1:正常, 0:禁用, -1:删除 Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_user_username_status,priority:2;index:idx_user_email_status,priority:2;index:idx_user_role_status,priority:2" json:"status"` // 1:正常, 0:禁用, -1:删除
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据存储为PostgreSQL的JSONB类型 Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty" swaggertype:"string"` // JSON数据存储为PostgreSQL的JSONB类型
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp;index:idx_user_last_login,sort:desc" json:"last_login_at,omitempty"` LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp;index:idx_user_last_login,sort:desc" json:"last_login_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_user_created_at,sort:desc" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_user_created_at,sort:desc" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
@@ -28,6 +29,7 @@ func (User) TableName() string {
} }
// UserPointLog 用户积分变更记录 // UserPointLog 用户积分变更记录
// @Description 用户积分变动日志记录
type UserPointLog struct { type UserPointLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index:idx_point_logs_user_created,priority:1" json:"user_id"` UserID int64 `gorm:"column:user_id;not null;index:idx_point_logs_user_created,priority:1" json:"user_id"`
@@ -52,6 +54,7 @@ func (UserPointLog) TableName() string {
} }
// UserLoginLog 用户登录日志 // UserLoginLog 用户登录日志
// @Description 用户登录历史记录
type UserLoginLog struct { type UserLoginLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index:idx_login_logs_user_created,priority:1" json:"user_id"` UserID int64 `gorm:"column:user_id;not null;index:idx_login_logs_user_created,priority:1" json:"user_id"`

View File

@@ -13,6 +13,7 @@ import (
const passwordChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" const passwordChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
// Yggdrasil ygg密码与用户id绑定 // Yggdrasil ygg密码与用户id绑定
// @Description Yggdrasil认证密码数据模型
type Yggdrasil struct { type Yggdrasil struct {
ID int64 `gorm:"column:id;primaryKey;not null" json:"id"` ID int64 `gorm:"column:id;primaryKey;not null" json:"id"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 加密后的密码,不返回给前端 Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 加密后的密码,不返回给前端

View File

@@ -0,0 +1,18 @@
package model
import (
"strings"
"testing"
)
func TestGenerateRandomPassword(t *testing.T) {
pwd := GenerateRandomPassword(16)
if len(pwd) != 16 {
t.Fatalf("length mismatch: %d", len(pwd))
}
for _, ch := range pwd {
if !strings.ContainsRune(passwordChars, ch) {
t.Fatalf("unexpected char: %c", ch)
}
}
}

View File

@@ -66,26 +66,6 @@ type TextureRepository interface {
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error) CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
} }
// TokenRepository 令牌仓储接口
type TokenRepository interface {
Create(ctx context.Context, token *model.Token) error
FindByAccessToken(ctx context.Context, accessToken string) (*model.Token, error)
GetByUserID(ctx context.Context, userId int64) ([]*model.Token, error)
GetUUIDByAccessToken(ctx context.Context, accessToken string) (string, error)
GetUserIDByAccessToken(ctx context.Context, accessToken string) (int64, error)
DeleteByAccessToken(ctx context.Context, accessToken string) error
DeleteByUserID(ctx context.Context, userId int64) error
BatchDelete(ctx context.Context, accessTokens []string) (int64, error)
}
// SystemConfigRepository 系统配置仓储接口
type SystemConfigRepository interface {
GetByKey(ctx context.Context, key string) (*model.SystemConfig, error)
GetPublic(ctx context.Context) ([]model.SystemConfig, error)
GetAll(ctx context.Context) ([]model.SystemConfig, error)
Update(ctx context.Context, config *model.SystemConfig) error
UpdateValue(ctx context.Context, key, value string) error
}
// YggdrasilRepository Yggdrasil仓储接口 // YggdrasilRepository Yggdrasil仓储接口
type YggdrasilRepository interface { type YggdrasilRepository interface {

View File

@@ -0,0 +1,253 @@
package repository
import (
"context"
"testing"
"carrotskin/internal/model"
"carrotskin/internal/testutil"
)
func TestUserRepository_BasicAndPoints(t *testing.T) {
db := testutil.NewTestDB(t)
repo := NewUserRepository(db)
ctx := context.Background()
user := &model.User{Username: "u1", Email: "e1@test.com", Password: "pwd", Status: 1}
if err := repo.Create(ctx, user); err != nil {
t.Fatalf("create user err: %v", err)
}
if u, err := repo.FindByID(ctx, user.ID); err != nil || u.Username != "u1" {
t.Fatalf("FindByID mismatch: %v %+v", err, u)
}
if u, err := repo.FindByUsername(ctx, "u1"); err != nil || u.Email != "e1@test.com" {
t.Fatalf("FindByUsername mismatch")
}
if u, err := repo.FindByEmail(ctx, "e1@test.com"); err != nil || u.ID != user.ID {
t.Fatalf("FindByEmail mismatch")
}
if err := repo.UpdateFields(ctx, user.ID, map[string]interface{}{"avatar": "a.png"}); err != nil {
t.Fatalf("UpdateFields err: %v", err)
}
if _, err := repo.BatchUpdate(ctx, []int64{user.ID}, map[string]interface{}{"status": 2}); err != nil {
t.Fatalf("BatchUpdate err: %v", err)
}
// 积分增加
if err := repo.UpdatePoints(ctx, user.ID, 10, "add", "bonus"); err != nil {
t.Fatalf("UpdatePoints add err: %v", err)
}
// 积分不足场景
if err := repo.UpdatePoints(ctx, user.ID, -100, "sub", "penalty"); err == nil {
t.Fatalf("expected insufficient points error")
}
if list, err := repo.FindByIDs(ctx, []int64{user.ID}); err != nil || len(list) != 1 {
t.Fatalf("FindByIDs mismatch: %v %d", err, len(list))
}
if list, err := repo.FindByIDs(ctx, []int64{}); err != nil || len(list) != 0 {
t.Fatalf("FindByIDs empty mismatch: %v %d", err, len(list))
}
// 软删除
if err := repo.Delete(ctx, user.ID); err != nil {
t.Fatalf("Delete err: %v", err)
}
deleted, _ := repo.FindByID(ctx, user.ID)
if deleted != nil {
t.Fatalf("expected deleted user filtered out")
}
// 批量操作边界
if _, err := repo.BatchUpdate(ctx, []int64{}, map[string]interface{}{"status": 1}); err != nil {
t.Fatalf("BatchUpdate empty should not error: %v", err)
}
if _, err := repo.BatchDelete(ctx, []int64{}); err != nil {
t.Fatalf("BatchDelete empty should not error: %v", err)
}
// 日志写入
_ = repo.CreateLoginLog(ctx, &model.UserLoginLog{UserID: user.ID, IPAddress: "127.0.0.1"})
_ = repo.CreatePointLog(ctx, &model.UserPointLog{UserID: user.ID, Amount: 1, ChangeType: "add"})
}
func TestProfileRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)
userRepo := NewUserRepository(db)
profileRepo := NewProfileRepository(db)
ctx := context.Background()
u := &model.User{Username: "u2", Email: "u2@test.com", Password: "pwd", Status: 1}
_ = userRepo.Create(ctx, u)
p := &model.Profile{UUID: "p-uuid", UserID: u.ID, Name: "hero"}
if err := profileRepo.Create(ctx, p); err != nil {
t.Fatalf("create profile err: %v", err)
}
if got, err := profileRepo.FindByUUID(ctx, "p-uuid"); err != nil || got.Name != "hero" {
t.Fatalf("FindByUUID mismatch: %v %+v", err, got)
}
if list, err := profileRepo.FindByUserID(ctx, u.ID); err != nil || len(list) != 1 {
t.Fatalf("FindByUserID mismatch")
}
if count, err := profileRepo.CountByUserID(ctx, u.ID); err != nil || count != 1 {
t.Fatalf("CountByUserID mismatch: %d err=%v", count, err)
}
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
t.Fatalf("UpdateLastUsedAt err: %v", err)
}
if got, err := profileRepo.FindByName(ctx, "hero"); err != nil || got == nil {
t.Fatalf("FindByName mismatch")
}
if list, err := profileRepo.FindByUUIDs(ctx, []string{"p-uuid"}); err != nil || len(list) != 1 {
t.Fatalf("FindByUUIDs mismatch")
}
if _, err := profileRepo.BatchUpdate(ctx, []string{"p-uuid"}, map[string]interface{}{"name": "hero2"}); err != nil {
t.Fatalf("BatchUpdate profile err: %v", err)
}
if err := profileRepo.Delete(ctx, "p-uuid"); err != nil {
t.Fatalf("Delete err: %v", err)
}
if _, err := profileRepo.BatchDelete(ctx, []string{}); err != nil {
t.Fatalf("BatchDelete empty err: %v", err)
}
}
func TestTextureRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)
userRepo := NewUserRepository(db)
textureRepo := NewTextureRepository(db)
ctx := context.Background()
u := &model.User{Username: "u3", Email: "u3@test.com", Password: "pwd", Status: 1}
_ = userRepo.Create(ctx, u)
tex := &model.Texture{
UploaderID: u.ID,
Name: "tex",
Hash: "hash1",
URL: "url1",
Type: model.TextureTypeSkin,
IsPublic: true,
Status: 1,
}
if err := textureRepo.Create(ctx, tex); err != nil {
t.Fatalf("create texture err: %v", err)
}
if got, _ := textureRepo.FindByHash(ctx, "hash1"); got == nil || got.ID != tex.ID {
t.Fatalf("FindByHash mismatch")
}
if got, _ := textureRepo.FindByHashAndUploaderID(ctx, "hash1", u.ID); got == nil {
t.Fatalf("FindByHashAndUploaderID mismatch")
}
_ = textureRepo.IncrementFavoriteCount(ctx, tex.ID)
_ = textureRepo.DecrementFavoriteCount(ctx, tex.ID)
_ = textureRepo.IncrementDownloadCount(ctx, tex.ID)
_ = textureRepo.CreateDownloadLog(ctx, &model.TextureDownloadLog{TextureID: tex.ID, UserID: &u.ID, IPAddress: "127.0.0.1"})
// 收藏
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID)
if fav, err := textureRepo.IsFavorited(ctx, u.ID, tex.ID); err == nil {
if !fav {
t.Fatalf("IsFavorited expected true")
}
} else {
t.Skipf("IsFavorited not supported by sqlite: %v", err)
}
_ = textureRepo.RemoveFavorite(ctx, u.ID, tex.ID)
// 批量更新与删除
if affected, err := textureRepo.BatchUpdate(ctx, []int64{tex.ID}, map[string]interface{}{"name": "tex-new"}); err != nil || affected != 1 {
t.Fatalf("BatchUpdate mismatch, affected=%d err=%v", affected, err)
}
if affected, err := textureRepo.BatchDelete(ctx, []int64{tex.ID}); err != nil || affected != 1 {
t.Fatalf("BatchDelete mismatch, affected=%d err=%v", affected, err)
}
// 搜索与收藏列表
_ = textureRepo.Create(ctx, &model.Texture{
UploaderID: u.ID,
Name: "search-me",
Hash: "hash2",
URL: "url2",
Type: model.TextureTypeCape,
IsPublic: true,
Status: 1,
})
if list, total, err := textureRepo.Search(ctx, "search", model.TextureTypeCape, true, 1, 10); err != nil || total == 0 || len(list) == 0 {
t.Fatalf("Search mismatch, total=%d len=%d err=%v", total, len(list), err)
}
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID+1)
if favList, total, err := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10); err != nil || total == 0 || len(favList) == 0 {
t.Fatalf("GetUserFavorites mismatch, total=%d len=%d err=%v", total, len(favList), err)
}
if _, total, err := textureRepo.Search(ctx, "", model.TextureTypeSkin, true, 1, 10); err != nil || total < 2 {
t.Fatalf("Search fallback mismatch")
}
// 列表与计数
if _, total, err := textureRepo.FindByUploaderID(ctx, u.ID, 1, 10); err != nil || total != 1 {
t.Fatalf("FindByUploaderID mismatch")
}
if cnt, err := textureRepo.CountByUploaderID(ctx, u.ID); err != nil || cnt != 1 {
t.Fatalf("CountByUploaderID mismatch")
}
_ = textureRepo.Delete(ctx, tex.ID)
}
func TestClientRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)
repo := NewClientRepository(db)
ctx := context.Background()
client := &model.Client{UUID: "c-uuid", ClientToken: "ct-1", UserID: 9, Version: 1}
if err := repo.Create(ctx, client); err != nil {
t.Fatalf("Create client err: %v", err)
}
if got, _ := repo.FindByClientToken(ctx, "ct-1"); got == nil || got.UUID != "c-uuid" {
t.Fatalf("FindByClientToken mismatch")
}
if got, _ := repo.FindByUUID(ctx, "c-uuid"); got == nil || got.ClientToken != "ct-1" {
t.Fatalf("FindByUUID mismatch")
}
if list, _ := repo.FindByUserID(ctx, 9); len(list) != 1 {
t.Fatalf("FindByUserID mismatch")
}
_ = repo.IncrementVersion(ctx, "c-uuid")
updated, _ := repo.FindByUUID(ctx, "c-uuid")
if updated.Version != 2 {
t.Fatalf("IncrementVersion not applied, got %d", updated.Version)
}
_ = repo.DeleteByClientToken(ctx, "ct-1")
_ = repo.DeleteByUserID(ctx, 9)
}
func TestYggdrasilRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)
userRepo := NewUserRepository(db)
yggRepo := NewYggdrasilRepository(db)
ctx := context.Background()
user := &model.User{Username: "u-ygg", Email: "ygg@test.com", Password: "pwd", Status: 1}
_ = userRepo.Create(ctx, user) // AfterCreate 会生成 yggdrasil 记录
pwd, err := yggRepo.GetPasswordByID(ctx, user.ID)
if err != nil || pwd == "" {
t.Fatalf("GetPasswordByID err=%v pwd=%s", err, pwd)
}
if err := yggRepo.ResetPassword(ctx, user.ID, "newpwd"); err != nil {
t.Fatalf("ResetPassword err: %v", err)
}
}

View File

@@ -1,44 +0,0 @@
package repository
import (
"carrotskin/internal/model"
"context"
"gorm.io/gorm"
)
// systemConfigRepository SystemConfigRepository的实现
type systemConfigRepository struct {
db *gorm.DB
}
// NewSystemConfigRepository 创建SystemConfigRepository实例
func NewSystemConfigRepository(db *gorm.DB) SystemConfigRepository {
return &systemConfigRepository{db: db}
}
func (r *systemConfigRepository) GetByKey(ctx context.Context, key string) (*model.SystemConfig, error) {
var config model.SystemConfig
err := r.db.WithContext(ctx).Where("key = ?", key).First(&config).Error
return handleNotFoundResult(&config, err)
}
func (r *systemConfigRepository) GetPublic(ctx context.Context) ([]model.SystemConfig, error) {
var configs []model.SystemConfig
err := r.db.WithContext(ctx).Where("is_public = ?", true).Find(&configs).Error
return configs, err
}
func (r *systemConfigRepository) GetAll(ctx context.Context) ([]model.SystemConfig, error) {
var configs []model.SystemConfig
err := r.db.WithContext(ctx).Find(&configs).Error
return configs, err
}
func (r *systemConfigRepository) Update(ctx context.Context, config *model.SystemConfig) error {
return r.db.WithContext(ctx).Save(config).Error
}
func (r *systemConfigRepository) UpdateValue(ctx context.Context, key, value string) error {
return r.db.WithContext(ctx).Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error
}

View File

@@ -1,146 +0,0 @@
package repository
import (
"testing"
)
// TestSystemConfigRepository_QueryConditions 测试系统配置查询条件逻辑
func TestSystemConfigRepository_QueryConditions(t *testing.T) {
tests := []struct {
name string
key string
isPublic bool
wantValid bool
}{
{
name: "有效的配置键",
key: "site_name",
isPublic: true,
wantValid: true,
},
{
name: "配置键为空",
key: "",
isPublic: true,
wantValid: false,
},
{
name: "公开配置查询",
key: "site_name",
isPublic: true,
wantValid: true,
},
{
name: "私有配置查询",
key: "secret_key",
isPublic: false,
wantValid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := tt.key != ""
if isValid != tt.wantValid {
t.Errorf("Query condition validation failed: got %v, want %v", isValid, tt.wantValid)
}
})
}
}
// TestSystemConfigRepository_PublicConfigLogic 测试公开配置逻辑
func TestSystemConfigRepository_PublicConfigLogic(t *testing.T) {
tests := []struct {
name string
isPublic bool
wantInclude bool
}{
{
name: "只获取公开配置",
isPublic: true,
wantInclude: true,
},
{
name: "私有配置不应包含",
isPublic: false,
wantInclude: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 验证逻辑GetPublicSystemConfigs应该只返回is_public=true的配置
if tt.isPublic != tt.wantInclude {
t.Errorf("Public config logic failed: isPublic=%v, wantInclude=%v", tt.isPublic, tt.wantInclude)
}
})
}
}
// TestSystemConfigRepository_UpdateValueLogic 测试更新配置值逻辑
func TestSystemConfigRepository_UpdateValueLogic(t *testing.T) {
tests := []struct {
name string
key string
value string
wantValid bool
}{
{
name: "有效的键值对",
key: "site_name",
value: "CarrotSkin",
wantValid: true,
},
{
name: "键为空",
key: "",
value: "CarrotSkin",
wantValid: false,
},
{
name: "值为空(可能有效)",
key: "site_name",
value: "",
wantValid: true, // 空值也可能是有效的
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := tt.key != ""
if isValid != tt.wantValid {
t.Errorf("Update value validation failed: got %v, want %v", isValid, tt.wantValid)
}
})
}
}
// TestSystemConfigRepository_ErrorHandling 测试错误处理逻辑
func TestSystemConfigRepository_ErrorHandling(t *testing.T) {
tests := []struct {
name string
isNotFound bool
wantNilConfig bool
}{
{
name: "记录未找到应该返回nil配置",
isNotFound: true,
wantNilConfig: true,
},
{
name: "找到记录应该返回配置",
isNotFound: false,
wantNilConfig: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 验证错误处理逻辑如果是RecordNotFound返回nil配置
if tt.isNotFound != tt.wantNilConfig {
t.Errorf("Error handling logic failed: isNotFound=%v, wantNilConfig=%v", tt.isNotFound, tt.wantNilConfig)
}
})
}
}

View File

@@ -1,71 +0,0 @@
package repository
import (
"carrotskin/internal/model"
"context"
"gorm.io/gorm"
)
// tokenRepository TokenRepository的实现
type tokenRepository struct {
db *gorm.DB
}
// NewTokenRepository 创建TokenRepository实例
func NewTokenRepository(db *gorm.DB) TokenRepository {
return &tokenRepository{db: db}
}
func (r *tokenRepository) Create(ctx context.Context, token *model.Token) error {
return r.db.WithContext(ctx).Create(token).Error
}
func (r *tokenRepository) FindByAccessToken(ctx context.Context, accessToken string) (*model.Token, error) {
var token model.Token
err := r.db.WithContext(ctx).Where("access_token = ?", accessToken).First(&token).Error
if err != nil {
return nil, err
}
return &token, nil
}
func (r *tokenRepository) GetByUserID(ctx context.Context, userId int64) ([]*model.Token, error) {
var tokens []*model.Token
err := r.db.WithContext(ctx).Where("user_id = ?", userId).Find(&tokens).Error
return tokens, err
}
func (r *tokenRepository) GetUUIDByAccessToken(ctx context.Context, accessToken string) (string, error) {
var token model.Token
err := r.db.WithContext(ctx).Select("profile_id").Where("access_token = ?", accessToken).First(&token).Error
if err != nil {
return "", err
}
return token.ProfileId, nil
}
func (r *tokenRepository) GetUserIDByAccessToken(ctx context.Context, accessToken string) (int64, error) {
var token model.Token
err := r.db.WithContext(ctx).Select("user_id").Where("access_token = ?", accessToken).First(&token).Error
if err != nil {
return 0, err
}
return token.UserID, nil
}
func (r *tokenRepository) DeleteByAccessToken(ctx context.Context, accessToken string) error {
return r.db.WithContext(ctx).Where("access_token = ?", accessToken).Delete(&model.Token{}).Error
}
func (r *tokenRepository) DeleteByUserID(ctx context.Context, userId int64) error {
return r.db.WithContext(ctx).Where("user_id = ?", userId).Delete(&model.Token{}).Error
}
func (r *tokenRepository) BatchDelete(ctx context.Context, accessTokens []string) (int64, error) {
if len(accessTokens) == 0 {
return 0, nil
}
result := r.db.WithContext(ctx).Where("access_token IN ?", accessTokens).Delete(&model.Token{})
return result.RowsAffected, result.Error
}

View File

@@ -1,123 +0,0 @@
package repository
import (
"testing"
)
// TestTokenRepository_BatchDeleteLogic 测试批量删除逻辑
func TestTokenRepository_BatchDeleteLogic(t *testing.T) {
tests := []struct {
name string
tokensToDelete []string
wantCount int64
wantError bool
}{
{
name: "有效的token列表",
tokensToDelete: []string{"token1", "token2", "token3"},
wantCount: 3,
wantError: false,
},
{
name: "空列表应该返回0",
tokensToDelete: []string{},
wantCount: 0,
wantError: false,
},
{
name: "单个token",
tokensToDelete: []string{"token1"},
wantCount: 1,
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 验证批量删除逻辑空列表应该直接返回0
if len(tt.tokensToDelete) == 0 {
if tt.wantCount != 0 {
t.Errorf("Empty list should return count 0, got %d", tt.wantCount)
}
}
})
}
}
// TestTokenRepository_QueryConditions 测试token查询条件逻辑
func TestTokenRepository_QueryConditions(t *testing.T) {
tests := []struct {
name string
accessToken string
userID int64
wantValid bool
}{
{
name: "有效的access token",
accessToken: "valid-token-123",
userID: 1,
wantValid: true,
},
{
name: "access token为空",
accessToken: "",
userID: 1,
wantValid: false,
},
{
name: "用户ID为0",
accessToken: "valid-token-123",
userID: 0,
wantValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := tt.accessToken != "" && tt.userID > 0
if isValid != tt.wantValid {
t.Errorf("Query condition validation failed: got %v, want %v", isValid, tt.wantValid)
}
})
}
}
// TestTokenRepository_FindTokenByIDLogic 测试根据ID查找token的逻辑
func TestTokenRepository_FindTokenByIDLogic(t *testing.T) {
tests := []struct {
name string
accessToken string
resultCount int
wantError bool
}{
{
name: "找到token",
accessToken: "token-123",
resultCount: 1,
wantError: false,
},
{
name: "未找到token",
accessToken: "token-123",
resultCount: 0,
wantError: true, // 访问索引0会panic
},
{
name: "找到多个token异常情况",
accessToken: "token-123",
resultCount: 2,
wantError: false, // 返回第一个
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 验证逻辑如果结果为空访问索引0会出错
hasError := tt.resultCount == 0
if hasError != tt.wantError {
t.Errorf("FindTokenByID logic failed: got error=%v, want error=%v", hasError, tt.wantError)
}
})
}
}

View File

@@ -136,69 +136,6 @@ type SecurityService interface {
ClearVerifyAttempts(ctx context.Context, email, codeType string) error ClearVerifyAttempts(ctx context.Context, email, codeType string) error
} }
// TextureRenderService 纹理渲染服务接口
type TextureRenderService interface {
// RenderTexture 渲染纹理为预览图
RenderTexture(ctx context.Context, textureID int64, renderType RenderType, size int, format ImageFormat) (*RenderResult, error)
// RenderTextureFromData 从原始数据渲染纹理
RenderTextureFromData(ctx context.Context, textureData []byte, renderType RenderType, size int, format ImageFormat, isSlim bool) ([]byte, string, error)
// GetRenderURL 获取渲染图的URL
GetRenderURL(textureID int64, renderType RenderType, size int, format ImageFormat) string
// DeleteRenderCache 删除渲染缓存
DeleteRenderCache(ctx context.Context, textureID int64) error
// RenderAvatar 渲染头像支持2D/3D模式
RenderAvatar(ctx context.Context, textureID int64, size int, mode AvatarMode, format ImageFormat) (*RenderResult, error)
// RenderCape 渲染披风
RenderCape(ctx context.Context, textureID int64, size int, format ImageFormat) (*RenderResult, error)
// RenderPreview 渲染预览图类似Blessing Skin的preview功能
RenderPreview(ctx context.Context, textureID int64, size int, format ImageFormat) (*RenderResult, error)
}
// RenderType 渲染类型
type RenderType string
const (
RenderTypeFront RenderType = "front" // 正面
RenderTypeBack RenderType = "back" // 背面
RenderTypeFull RenderType = "full" // 全身
RenderTypeHead RenderType = "head" // 头像
RenderTypeIsometric RenderType = "isometric" // 等距视图
)
// ImageFormat 输出格式
type ImageFormat string
const (
ImageFormatPNG ImageFormat = "png"
ImageFormatWEBP ImageFormat = "webp"
)
// AvatarMode 头像模式
type AvatarMode string
const (
AvatarMode2D AvatarMode = "2d" // 2D头像
AvatarMode3D AvatarMode = "3d" // 3D头像
)
// TextureType 纹理类型
type TextureType string
const (
TextureTypeSteve TextureType = "steve" // Steve皮肤
TextureTypeAlex TextureType = "alex" // Alex皮肤
TextureTypeCape TextureType = "cape" // 披风
)
// RenderResult 渲染结果(附带缓存/HTTP头信息
type RenderResult struct {
URL string
ContentType string
ETag string
LastModified time.Time
Size int64
}
// Services 服务集合 // Services 服务集合
type Services struct { type Services struct {
User UserService User UserService

Some files were not shown because too many files have changed in this diff Show More