chore(git): 更新.gitignore以忽略新的本地文件
Some checks failed
SonarQube Analysis / sonarqube (push) Has been cancelled

This commit is contained in:
lan
2025-11-30 08:33:17 +08:00
parent 4b4980820f
commit a4b6c5011e
58 changed files with 19353 additions and 0 deletions

View File

@@ -0,0 +1,452 @@
# 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

@@ -0,0 +1,386 @@
# 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

@@ -0,0 +1,305 @@
# 会话服务
<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

@@ -0,0 +1,309 @@
# 档案查询服务
<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

@@ -0,0 +1,464 @@
# 认证服务
<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

@@ -0,0 +1,284 @@
# 分页处理机制
<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

@@ -0,0 +1,431 @@
# 材质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

@@ -0,0 +1,145 @@
# 材质上传与管理
<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

@@ -0,0 +1,327 @@
# 材质上传流程
<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

@@ -0,0 +1,460 @@
# 材质管理操作
<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

@@ -0,0 +1,306 @@
# 材质删除
<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

@@ -0,0 +1,243 @@
# 材质搜索
<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

@@ -0,0 +1,411 @@
# 材质收藏
<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

@@ -0,0 +1,398 @@
# 创建与列表
<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

@@ -0,0 +1,354 @@
# 更新与删除
<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

@@ -0,0 +1,336 @@
# 档案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

@@ -0,0 +1,288 @@
# 激活管理
<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

@@ -0,0 +1,299 @@
# 详情查询
<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

@@ -0,0 +1,357 @@
# 个人资料管理
<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

@@ -0,0 +1,337 @@
# 头像管理
<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

@@ -0,0 +1,384 @@
# 用户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

@@ -0,0 +1,336 @@
# 邮箱变更
<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

@@ -0,0 +1,261 @@
# 系统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

@@ -0,0 +1,330 @@
# 发送验证码
<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

@@ -0,0 +1,352 @@
# 用户注册
<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

@@ -0,0 +1,351 @@
# 用户登录
<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

@@ -0,0 +1,460 @@
# 认证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

@@ -0,0 +1,343 @@
# 重置密码
<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

@@ -0,0 +1,294 @@
# 验证码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)