Compare commits
23 Commits
feature/re
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e8b7d150d | ||
|
|
fd5a0e8405 | ||
|
|
573c10ed1d | ||
|
|
3b8d8bd7a7 | ||
|
|
6338592d27 | ||
|
|
ef460ec891 | ||
|
|
62d9432a2d | ||
|
|
e1d79ed445 | ||
|
|
c5d7e317a4 | ||
|
|
06539dc086 | ||
|
|
22142db782 | ||
|
|
2c9c6ecfc0 | ||
|
|
c5db489d72 | ||
| d952ddd4ea | |||
| e761ff5be5 | |||
| 9e83ae16af | |||
| 85a9463913 | |||
| 44f007936e | |||
| 9b0a60033e | |||
| 399e6f096f | |||
| 63ca7eff0d | |||
| aa75691c49 | |||
|
|
a51535a465 |
@@ -73,10 +73,3 @@ scripts/
|
||||
local/
|
||||
dev/
|
||||
minio-data/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
# ==================== CarrotSkin Docker 环境配置示例 ====================
|
||||
# 复制此文件为 .env 后修改配置值
|
||||
# 此文件用于 docker-compose 部署,变量名与 docker-compose.yml 中的引用一致
|
||||
|
||||
# ==================== 站点配置 ====================
|
||||
SITE_NAME=CarrotSkin
|
||||
SITE_DESCRIPTION=一个优秀的Minecraft皮肤站
|
||||
REGISTRATION_ENABLED=true
|
||||
DEFAULT_AVATAR=
|
||||
|
||||
# ==================== 用户限制配置 ====================
|
||||
MAX_TEXTURES_PER_USER=50
|
||||
MAX_PROFILES_PER_USER=5
|
||||
|
||||
# ==================== 积分配置 ====================
|
||||
CHECKIN_REWARD=10
|
||||
TEXTURE_DOWNLOAD_REWARD=1
|
||||
|
||||
# ==================== 服务配置 ====================
|
||||
# 应用端口
|
||||
# 应用对外端口
|
||||
APP_PORT=8080
|
||||
# 运行模式: debug, release, test
|
||||
SERVER_MODE=release
|
||||
# API 根路径 (用于反向代理,如 /api)
|
||||
SERVER_BASE_PATH=
|
||||
# 公开访问地址 (用于生成回调URL、邮件链接等)
|
||||
PUBLIC_URL=http://localhost:8080
|
||||
# 是否启用 Swagger 文档: true, false
|
||||
SERVER_SWAGGER_ENABLED=true
|
||||
|
||||
# ==================== 数据库配置 ====================
|
||||
DB_PASSWORD=carrotskin123
|
||||
# 数据库密码,生产环境务必修改
|
||||
DATABASE_PASSWORD=carrotskin123
|
||||
|
||||
# ==================== Redis 配置 ====================
|
||||
# 留空表示不设置密码
|
||||
@@ -25,23 +39,26 @@ JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
# ==================== 存储配置 (RustFS S3兼容) ====================
|
||||
# 内部访问地址 (容器间通信)
|
||||
RUSTFS_ENDPOINT=rustfs:9000
|
||||
# 公开访问地址 (用于生成文件URL,供外部浏览器访问)
|
||||
# 示例: 直接访问 http://localhost:9000 或反向代理 https://example.com/storage
|
||||
RUSTFS_PUBLIC_URL=http://localhost:9000
|
||||
RUSTFS_ACCESS_KEY=rustfsadmin
|
||||
RUSTFS_SECRET_KEY=rustfsadmin123
|
||||
RUSTFS_USE_SSL=false
|
||||
|
||||
# 存储桶配置
|
||||
RUSTFS_BUCKET_TEXTURES=carrotskin
|
||||
RUSTFS_BUCKET_AVATARS=carrotskin
|
||||
RUSTFS_BUCKET_TEXTURES=carrot-skin-textures
|
||||
RUSTFS_BUCKET_AVATARS=carrot-skin-avatars
|
||||
|
||||
# 公开访问地址 (用于生成文件URL,供外部浏览器访问)
|
||||
# 示例:
|
||||
# 直接访问: http://localhost:9000
|
||||
# 反向代理: https://example.com/storage
|
||||
RUSTFS_PUBLIC_URL=http://localhost:9000
|
||||
# ==================== 安全配置 ====================
|
||||
# CORS 允许的来源,多个用逗号分隔
|
||||
SECURITY_ALLOWED_ORIGINS=*
|
||||
# 允许的头像/材质URL域名,多个用逗号分隔
|
||||
SECURITY_ALLOWED_DOMAINS=localhost,127.0.0.1
|
||||
|
||||
# ==================== 邮件配置 (可选) ====================
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=
|
||||
# ==================== 邮件配置 ====================
|
||||
EMAIL_ENABLED=false
|
||||
EMAIL_SMTP_HOST=
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_USERNAME=
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_FROM_NAME=CarrotSkin
|
||||
|
||||
42
.env.example
42
.env.example
@@ -1,6 +1,26 @@
|
||||
# CarrotSkin 环境配置文件示例
|
||||
# 复制此文件为 .env 并修改相应的配置值
|
||||
|
||||
# =============================================================================
|
||||
# 站点配置
|
||||
# =============================================================================
|
||||
SITE_NAME=CarrotSkin
|
||||
SITE_DESCRIPTION=一个优秀的Minecraft皮肤站
|
||||
REGISTRATION_ENABLED=true
|
||||
DEFAULT_AVATAR=
|
||||
|
||||
# =============================================================================
|
||||
# 用户限制配置
|
||||
# =============================================================================
|
||||
MAX_TEXTURES_PER_USER=50
|
||||
MAX_PROFILES_PER_USER=5
|
||||
|
||||
# =============================================================================
|
||||
# 积分配置
|
||||
# =============================================================================
|
||||
CHECKIN_REWARD=10
|
||||
TEXTURE_DOWNLOAD_REWARD=1
|
||||
|
||||
# =============================================================================
|
||||
# 服务器配置
|
||||
# =============================================================================
|
||||
@@ -8,6 +28,7 @@ SERVER_PORT=:8080
|
||||
SERVER_MODE=debug
|
||||
SERVER_READ_TIMEOUT=30s
|
||||
SERVER_WRITE_TIMEOUT=30s
|
||||
SERVER_SWAGGER_ENABLED=true
|
||||
|
||||
# =============================================================================
|
||||
# 数据库配置
|
||||
@@ -23,6 +44,7 @@ DATABASE_TIMEZONE=Asia/Shanghai
|
||||
DATABASE_MAX_IDLE_CONNS=10
|
||||
DATABASE_MAX_OPEN_CONNS=100
|
||||
DATABASE_CONN_MAX_LIFETIME=1h
|
||||
DATABASE_CONN_MAX_IDLE_TIME=10m
|
||||
|
||||
# =============================================================================
|
||||
# Redis配置
|
||||
@@ -37,6 +59,7 @@ REDIS_POOL_SIZE=10
|
||||
# RustFS对象存储配置 (S3兼容)
|
||||
# =============================================================================
|
||||
RUSTFS_ENDPOINT=127.0.0.1:9000
|
||||
RUSTFS_PUBLIC_URL=http://127.0.0.1:9000
|
||||
RUSTFS_ACCESS_KEY=your_access_key
|
||||
RUSTFS_SECRET_KEY=your_secret_key
|
||||
RUSTFS_USE_SSL=false
|
||||
@@ -55,26 +78,17 @@ JWT_EXPIRE_HOURS=168
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
LOG_OUTPUT=logs/app.log
|
||||
LOG_MAX_SIZE=100
|
||||
LOG_MAX_BACKUPS=3
|
||||
LOG_MAX_AGE=28
|
||||
LOG_COMPRESS=true
|
||||
|
||||
# =============================================================================
|
||||
# 文件上传配置
|
||||
# =============================================================================
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_TEXTURE_MAX_SIZE=2097152
|
||||
UPLOAD_AVATAR_MAX_SIZE=1048576
|
||||
|
||||
# =============================================================================
|
||||
# 安全配置
|
||||
# =============================================================================
|
||||
MAX_LOGIN_ATTEMPTS=5
|
||||
LOGIN_LOCK_DURATION=30m
|
||||
# CORS 允许的来源,多个用逗号分隔
|
||||
SECURITY_ALLOWED_ORIGINS=*
|
||||
# 允许的头像/材质URL域名,多个用逗号分隔
|
||||
SECURITY_ALLOWED_DOMAINS=localhost,127.0.0.1
|
||||
|
||||
# =============================================================================
|
||||
# 邮件配置(可选)
|
||||
# 邮件配置
|
||||
# 腾讯企业邮箱SSL配置示例:smtp.exmail.qq.com, 端口465
|
||||
# =============================================================================
|
||||
EMAIL_ENABLED=false
|
||||
|
||||
73
.gitea/workflows/build.yml
Normal file
73
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: false
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: 0
|
||||
run: go build -v -o mcauth-linux-amd64 ./cmd/server
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mcauth-linux-amd64
|
||||
path: mcauth-linux-amd64
|
||||
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: code.littlelan.cn
|
||||
username: ${{ secrets.GIT_USERNAME }}
|
||||
password: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: mcauth-linux-amd64
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
code.littlelan.cn/carrotskin/mcauth:latest
|
||||
code.littlelan.cn/carrotskin/mcauth:${{ github.sha }}
|
||||
platforms: linux/amd64
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -23,8 +23,8 @@ dist/
|
||||
build/
|
||||
|
||||
# Compiled binaries
|
||||
/server
|
||||
server.exe
|
||||
main.exe
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
@@ -108,3 +108,5 @@ local/
|
||||
dev/
|
||||
service_coverage
|
||||
.gitignore
|
||||
docs/
|
||||
blessing skin材质渲染示例/
|
||||
|
||||
@@ -1,452 +0,0 @@
|
||||
# API参考
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [swagger.go](file://internal/handler/swagger.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go)
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本API参考面向CarrotSkin后端服务,覆盖认证、用户、材质、档案、验证码、Yggdrasil与系统配置等API组。文档基于routes.go与swagger.go中的路由与注释,结合各处理器文件的注释与类型定义,提供:
|
||||
- 端点清单与HTTP方法、URL模式
|
||||
- 请求/响应模式与鉴权要求(JWT)
|
||||
- 错误码与常见错误场景
|
||||
- 参数校验规则与示例
|
||||
- 性能优化建议与最佳实践
|
||||
|
||||
## 项目结构
|
||||
- 路由注册集中在路由文件,按版本分组与鉴权中间件组合
|
||||
- Swagger文档与健康检查在统一入口启用
|
||||
- 各功能模块处理器文件通过注释提供OpenAPI规范
|
||||
- 中间件负责JWT鉴权与可选鉴权
|
||||
- 响应模型与类型定义集中于model与types目录
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A["Gin引擎"] --> B["Swagger文档<br/>/swagger/*any"]
|
||||
A --> C["健康检查<br/>/health"]
|
||||
A --> D["API v1 组<br/>/api/v1"]
|
||||
D --> E["认证组<br/>/api/v1/auth/*"]
|
||||
D --> F["用户组<br/>/api/v1/user/*"]
|
||||
D --> G["材质组<br/>/api/v1/texture/*"]
|
||||
D --> H["档案组<br/>/api/v1/profile/*"]
|
||||
D --> I["验证码组<br/>/api/v1/captcha/*"]
|
||||
D --> J["Yggdrasil组<br/>/api/v1/yggdrasil/*"]
|
||||
D --> K["系统组<br/>/api/v1/system/*"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L10-L118)
|
||||
- [swagger.go](file://internal/handler/swagger.go#L41-L63)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L10-L118)
|
||||
- [swagger.go](file://internal/handler/swagger.go#L41-L63)
|
||||
|
||||
## 核心组件
|
||||
- 路由注册器:按版本分组,按需挂载鉴权中间件
|
||||
- Swagger:提供交互式文档与健康检查
|
||||
- 鉴权中间件:统一从Authorization头解析Bearer Token并注入用户上下文
|
||||
- 响应模型:统一的响应结构与分页结构
|
||||
- 类型定义:请求/响应结构体与约束规则
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L10-L118)
|
||||
- [swagger.go](file://internal/handler/swagger.go#L41-L63)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
- [common.go](file://internal/types/common.go#L1-L215)
|
||||
|
||||
## 架构总览
|
||||
下图展示API调用链与鉴权流程:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Router as "Gin路由"
|
||||
participant MW as "鉴权中间件"
|
||||
participant Handler as "处理器"
|
||||
participant Service as "服务层"
|
||||
participant DB as "数据库/缓存"
|
||||
Client->>Router : "HTTP请求"
|
||||
Router->>MW : "匹配路由并执行中间件"
|
||||
MW->>MW : "解析Authorization头"
|
||||
MW->>MW : "校验JWT并注入用户上下文"
|
||||
MW-->>Router : "通过或拒绝"
|
||||
Router->>Handler : "转发到对应处理器"
|
||||
Handler->>Service : "调用业务逻辑"
|
||||
Service->>DB : "读写数据"
|
||||
DB-->>Service : "返回结果"
|
||||
Service-->>Handler : "返回业务结果"
|
||||
Handler-->>Client : "统一响应结构"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [routes.go](file://internal/handler/routes.go#L10-L118)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 认证组(/api/v1/auth)
|
||||
- 无需JWT
|
||||
- 端点
|
||||
- POST /api/v1/auth/register
|
||||
- 请求体:RegisterRequest
|
||||
- 响应:LoginResponse(含token与用户信息)
|
||||
- 错误:400(参数错误)、401(登录失败)
|
||||
- POST /api/v1/auth/login
|
||||
- 请求体:LoginRequest(支持用户名或邮箱)
|
||||
- 响应:LoginResponse
|
||||
- 错误:400(参数错误)、401(登录失败)
|
||||
- POST /api/v1/auth/send-code
|
||||
- 请求体:SendVerificationCodeRequest(类型枚举:register/reset_password/change_email)
|
||||
- 响应:通用成功响应
|
||||
- 错误:400(参数错误)
|
||||
- POST /api/v1/auth/reset-password
|
||||
- 请求体:ResetPasswordRequest(验证码6位)
|
||||
- 响应:通用成功响应
|
||||
- 错误:400(参数错误)
|
||||
|
||||
参数与校验要点
|
||||
- RegisterRequest:用户名、邮箱、密码、验证码、可选头像URL
|
||||
- LoginRequest:用户名或邮箱、密码
|
||||
- SendVerificationCodeRequest:邮箱、类型
|
||||
- ResetPasswordRequest:邮箱、验证码、新密码
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L18-L25)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
|
||||
- [common.go](file://internal/types/common.go#L27-L66)
|
||||
|
||||
### 用户组(/api/v1/user,需JWT)
|
||||
- GET /api/v1/user/profile
|
||||
- 响应:UserInfo
|
||||
- 错误:401(未授权)、404(用户不存在)
|
||||
- PUT /api/v1/user/profile
|
||||
- 请求体:UpdateUserRequest(修改密码需同时提供旧密码与新密码)
|
||||
- 响应:UserInfo
|
||||
- 错误:400(参数错误)、401(未授权)、404(用户不存在)、500(服务器错误)
|
||||
- POST /api/v1/user/avatar/upload-url
|
||||
- 请求体:GenerateAvatarUploadURLRequest(文件名)
|
||||
- 响应:GenerateAvatarUploadURLResponse(含post_url、form_data、avatar_url、expires_in)
|
||||
- 错误:400(参数错误)
|
||||
- PUT /api/v1/user/avatar
|
||||
- 查询参数:avatar_url
|
||||
- 响应:UserInfo
|
||||
- 错误:400(参数错误)
|
||||
- POST /api/v1/user/change-email
|
||||
- 请求体:ChangeEmailRequest(新邮箱+验证码)
|
||||
- 响应:UserInfo
|
||||
- 错误:400(参数错误)、401(未授权)
|
||||
|
||||
参数与校验要点
|
||||
- UpdateUserRequest:头像URL、旧密码、新密码(二者需同时提供)
|
||||
- GenerateAvatarUploadURLRequest:文件名必填
|
||||
- ChangeEmailRequest:新邮箱、验证码
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L27-L41)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L17-L68)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L70-L193)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L195-L253)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L255-L326)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||||
- [common.go](file://internal/types/common.go#L42-L80)
|
||||
- [common.go](file://internal/types/common.go#L107-L125)
|
||||
- [common.go](file://internal/types/common.go#L167-L179)
|
||||
|
||||
### 材质组(/api/v1/texture)
|
||||
- 公开端点(无需JWT)
|
||||
- GET /api/v1/texture
|
||||
- 查询:keyword、type(SKIN/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
|
||||
- 请求体:AuthenticateRequest(agent、clientToken、identifier、password、requestUser)
|
||||
- 响应:AccessToken、ClientToken、SelectedProfile、AvailableProfiles、User(可选)
|
||||
- POST /api/v1/yggdrasil/authserver/validate
|
||||
- 请求体:ValidTokenRequest(accessToken、clientToken)
|
||||
- 响应:204(有效)或403(无效)
|
||||
- POST /api/v1/yggdrasil/authserver/refresh
|
||||
- 请求体:RefreshRequest(accessToken、clientToken、requestUser、selectedProfile)
|
||||
- 响应:新的AccessToken、ClientToken、SelectedProfile、User(可选)
|
||||
- POST /api/v1/yggdrasil/authserver/invalidate
|
||||
- 请求体:ValidTokenRequest(accessToken、clientToken)
|
||||
- 响应:204
|
||||
- POST /api/v1/yggdrasil/authserver/signout
|
||||
- 请求体:SignOutRequest(username、password)
|
||||
- 响应:204
|
||||
- 会话服务
|
||||
- GET /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid}
|
||||
- 响应:序列化后的档案信息
|
||||
- POST /api/v1/yggdrasil/sessionserver/session/minecraft/join
|
||||
- 请求体:JoinServerRequest(serverId、accessToken、selectedProfile)
|
||||
- 响应:204 或错误
|
||||
- GET /api/v1/yggdrasil/sessionserver/session/minecraft/hasJoined
|
||||
- 查询:serverId、username、ip
|
||||
- 响应:200(Profile)或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)
|
||||
- 无效token(401)
|
||||
- 参数错误
|
||||
- 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)
|
||||
@@ -1,386 +0,0 @@
|
||||
# Yggdrasil协议API
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
|
||||
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go)
|
||||
- [yggdrasil.go](file://internal/model/yggdrasil.go)
|
||||
- [token.go](file://internal/model/token.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [Yggdrasil路由结构](#yggdrasil路由结构)
|
||||
3. [认证服务API](#认证服务api)
|
||||
4. [会话服务API](#会话服务api)
|
||||
5. [档案服务API](#档案服务api)
|
||||
6. [与Minecraft客户端交互流程](#与minecraft客户端交互流程)
|
||||
7. [内部用户系统集成](#内部用户系统集成)
|
||||
8. [错误处理](#错误处理)
|
||||
9. [元数据与证书](#元数据与证书)
|
||||
|
||||
## 简介
|
||||
Yggdrasil协议是Minecraft客户端认证的核心接口,本系统实现了完整的Yggdrasil协议集成,为Minecraft玩家提供认证、会话管理和档案查询服务。该实现通过`/yggdrasil`路由组暴露标准API端点,与Minecraft客户端无缝对接,同时将Yggdrasil请求映射到系统的内部用户体系。
|
||||
|
||||
**Section sources**
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||||
|
||||
## Yggdrasil路由结构
|
||||
系统在`/api/v1/yggdrasil`路径下提供了完整的Yggdrasil协议支持,包含三个主要子路由组:`authserver`、`sessionserver`和`profiles`,分别处理认证、会话和档案相关请求。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Yggdrasil[Yggdrasil根路由 /yggdrasil]
|
||||
subgraph AuthServer
|
||||
Authenticate[POST /authserver/authenticate]
|
||||
Validate[POST /authserver/validate]
|
||||
Refresh[POST /authserver/refresh]
|
||||
Invalidate[POST /authserver/invalidate]
|
||||
SignOut[POST /authserver/signout]
|
||||
end
|
||||
subgraph SessionServer
|
||||
GetProfile[GET /sessionserver/session/minecraft/profile/:uuid]
|
||||
JoinServer[POST /sessionserver/session/minecraft/join]
|
||||
HasJoined[GET /sessionserver/session/minecraft/hasJoined]
|
||||
end
|
||||
subgraph Profiles
|
||||
GetProfilesByName[POST /api/profiles/minecraft]
|
||||
end
|
||||
Yggdrasil --> Authenticate
|
||||
Yggdrasil --> Validate
|
||||
Yggdrasil --> Refresh
|
||||
Yggdrasil --> Invalidate
|
||||
Yggdrasil --> SignOut
|
||||
Yggdrasil --> GetProfile
|
||||
Yggdrasil --> JoinServer
|
||||
Yggdrasil --> HasJoined
|
||||
Yggdrasil --> GetProfilesByName
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||||
|
||||
**Section sources**
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||||
|
||||
## 认证服务API
|
||||
认证服务API位于`/yggdrasil/authserver`路径下,提供用户认证和令牌管理功能。
|
||||
|
||||
### authenticate
|
||||
用户认证端点,用于验证用户凭据并获取访问令牌。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/authserver/authenticate`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"username": "用户邮箱或用户名",
|
||||
"password": "密码",
|
||||
"clientToken": "客户端令牌"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
- 200: 返回包含`accessToken`、`availableProfiles`和`selectedProfile`的认证响应
|
||||
- 403: 认证失败
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L246)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L204-L209)
|
||||
|
||||
### validate
|
||||
验证访问令牌的有效性。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/authserver/validate`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"accessToken": "访问令牌"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
- 204: 令牌有效
|
||||
- 403: 令牌无效
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L248-L267)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L260-L261)
|
||||
|
||||
### refresh
|
||||
刷新访问令牌,延长会话有效期。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/authserver/refresh`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"accessToken": "当前访问令牌",
|
||||
"clientToken": "客户端令牌",
|
||||
"selectedProfile": "选定的档案"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
- 200: 返回新的`accessToken`和`selectedProfile`
|
||||
- 400: 刷新失败
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L269-L361)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L341-L351)
|
||||
|
||||
### invalidate
|
||||
使访问令牌失效。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/authserver/invalidate`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"accessToken": "要使失效的令牌"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
- 204: 成功使令牌失效
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L363-L378)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L375)
|
||||
|
||||
### signout
|
||||
用户登出,使该用户的所有令牌失效。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/authserver/signout`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"username": "用户邮箱",
|
||||
"password": "密码"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
- 204: 登出成功
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L380-L425)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L422)
|
||||
|
||||
## 会话服务API
|
||||
会话服务API位于`/yggdrasil/sessionserver`路径下,管理玩家与服务器的会话。
|
||||
|
||||
### JoinServer
|
||||
记录玩家加入服务器的会话信息。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/sessionserver/session/minecraft/join`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"accessToken": "访问令牌",
|
||||
"selectedProfile": "选定的档案UUID",
|
||||
"serverId": "服务器ID"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
- 204: 加入成功
|
||||
- 500: 加入失败
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "Minecraft客户端"
|
||||
participant SessionServer as "会话服务"
|
||||
participant Redis as "Redis存储"
|
||||
Client->>SessionServer : POST /join
|
||||
SessionServer->>SessionServer : 验证accessToken
|
||||
SessionServer->>SessionServer : 验证selectedProfile
|
||||
SessionServer->>Redis : 存储会话数据
|
||||
Redis-->>SessionServer : 存储成功
|
||||
SessionServer-->>Client : 204 No Content
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
|
||||
|
||||
### HasJoinedServer
|
||||
验证玩家是否已加入指定服务器。
|
||||
|
||||
- **方法**: GET
|
||||
- **路径**: `/yggdrasil/sessionserver/session/minecraft/hasJoined`
|
||||
- **查询参数**:
|
||||
- `serverId`: 服务器ID
|
||||
- `username`: 用户名
|
||||
- `ip`: IP地址(可选)
|
||||
- **响应**:
|
||||
- 200: 返回玩家档案信息
|
||||
- 204: 未加入或验证失败
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
|
||||
|
||||
## 档案服务API
|
||||
档案服务API提供玩家档案查询功能。
|
||||
|
||||
### GetProfileByUUID
|
||||
根据UUID获取玩家档案。
|
||||
|
||||
- **方法**: GET
|
||||
- **路径**: `/yggdrasil/sessionserver/session/minecraft/profile/:uuid`
|
||||
- **响应**:
|
||||
- 200: 返回玩家档案信息
|
||||
- 500: 获取失败
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L427-L447)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L71-L80)
|
||||
|
||||
### GetProfilesByName
|
||||
根据用户名批量获取玩家档案。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/api/profiles/minecraft`
|
||||
- **请求体**: 用户名数组
|
||||
```json
|
||||
["player1", "player2"]
|
||||
```
|
||||
- **响应**: 返回匹配的档案列表
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L554-L587)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L129-L137)
|
||||
|
||||
## 与Minecraft客户端交互流程
|
||||
Minecraft客户端与Yggdrasil服务的完整交互流程如下:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "Minecraft客户端"
|
||||
participant AuthServer as "认证服务"
|
||||
participant SessionServer as "会话服务"
|
||||
participant ProfileServer as "档案服务"
|
||||
Client->>AuthServer : authenticate(邮箱/用户名, 密码)
|
||||
AuthServer-->>Client : accessToken, availableProfiles
|
||||
Client->>SessionServer : join(serverId, accessToken, selectedProfile)
|
||||
SessionServer-->>Client : 204
|
||||
Client->>ProfileServer : hasJoined(serverId, username)
|
||||
ProfileServer-->>Client : 玩家档案
|
||||
Client->>ProfileServer : getProfile(uuid)
|
||||
ProfileServer-->>Client : 档案详情
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
|
||||
|
||||
## 内部用户系统集成
|
||||
Yggdrasil实现与系统内部用户体系的集成通过以下方式完成:
|
||||
|
||||
### 用户映射
|
||||
系统通过`Yggdrasil`模型将Yggdrasil协议的密码认证与内部用户ID关联。当用户注册时,系统自动生成一个16位随机密码与用户ID绑定。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class User {
|
||||
+int64 ID
|
||||
+string Username
|
||||
+string Email
|
||||
+string Password
|
||||
}
|
||||
class Yggdrasil {
|
||||
+int64 ID
|
||||
+string Password
|
||||
}
|
||||
User --> Yggdrasil : "1对1关联"
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil.go](file://internal/model/yggdrasil.go)
|
||||
- [user_service.go](file://internal/service/user_service.go#L13-L67)
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil.go](file://internal/model/yggdrasil.go#L13-L38)
|
||||
- [user_service.go](file://internal/service/user_service.go#L13-L67)
|
||||
|
||||
### 令牌管理
|
||||
系统使用`Token`模型管理访问令牌,将`accessToken`与`userID`、`profileId`关联,实现会话状态管理。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Token {
|
||||
+string AccessToken
|
||||
+int64 UserID
|
||||
+string ProfileId
|
||||
+bool Usable
|
||||
}
|
||||
class Profile {
|
||||
+string UUID
|
||||
+int64 UserID
|
||||
+string Name
|
||||
}
|
||||
Token --> Profile : "关联档案"
|
||||
Token --> User : "关联用户"
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [token.go](file://internal/model/token.go)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L211-L216)
|
||||
|
||||
**Section sources**
|
||||
- [token.go](file://internal/model/token.go)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L211-L216)
|
||||
|
||||
## 错误处理
|
||||
系统实现了标准化的错误处理机制,所有Yggdrasil端点返回一致的错误响应格式。
|
||||
|
||||
### 常见错误代码
|
||||
| HTTP状态码 | 错误类型 | 描述 |
|
||||
|-----------|---------|------|
|
||||
| 400 | Bad Request | 请求格式无效 |
|
||||
| 403 | Forbidden | 认证失败或权限不足 |
|
||||
| 404 | Not Found | 资源未找到 |
|
||||
| 500 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
### 错误响应示例
|
||||
```json
|
||||
{
|
||||
"error": "密码错误"
|
||||
}
|
||||
```
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L19-L57)
|
||||
|
||||
## 元数据与证书
|
||||
系统提供元数据和玩家证书服务,支持Minecraft客户端的高级功能。
|
||||
|
||||
### GetMetaData
|
||||
获取服务元数据。
|
||||
|
||||
- **方法**: GET
|
||||
- **路径**: `/yggdrasil`
|
||||
- **响应**: 包含实现信息、链接和功能标志
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L589-L615)
|
||||
|
||||
### GetPlayerCertificates
|
||||
获取玩家证书。
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/yggdrasil/minecraftservices/player/certificates`
|
||||
- **认证**: Bearer Token
|
||||
- **响应**: 包含玩家公钥证书
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L618-L667)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L44-L48)
|
||||
@@ -1,305 +0,0 @@
|
||||
# 会话服务
|
||||
|
||||
<cite>
|
||||
**本文档引用文件**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
|
||||
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [API路由结构](#api路由结构)
|
||||
3. [核心API功能详解](#核心api功能详解)
|
||||
4. [会话数据结构](#会话数据结构)
|
||||
5. [反作弊机制](#反作弊机制)
|
||||
6. [与Minecraft客户端交互流程](#与minecraft客户端交互流程)
|
||||
7. [错误处理与日志](#错误处理与日志)
|
||||
8. [测试验证](#测试验证)
|
||||
|
||||
## 简介
|
||||
本文档详细描述了Yggdrasil会话服务的核心功能,重点聚焦于`/sessionserver`路由组下的三个关键API:`GetProfileByUUID`、`JoinServer`和`HasJoinedServer`。这些API构成了Minecraft服务器会话验证系统的核心,负责处理玩家加入服务器的会话建立、验证和玩家档案查询。
|
||||
|
||||
系统通过Redis存储会话数据,利用`Join_`为前缀的键名和15分钟的TTL(生存时间)来管理会话生命周期。整个流程确保了只有经过身份验证的玩家才能加入服务器,同时通过IP地址和用户名的双重验证机制防止作弊行为。
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L1-L100)
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||||
|
||||
## API路由结构
|
||||
会话服务的API路由定义在`routes.go`文件中,位于`/yggdrasil/sessionserver`路径下。该路由组提供了三个核心端点:
|
||||
|
||||
- `GET /session/minecraft/profile/:uuid`:根据玩家UUID获取其公开档案信息。
|
||||
- `POST /session/minecraft/join`:接收客户端的会话信息,建立服务器会话。
|
||||
- `GET /session/minecraft/hasJoined`:验证某玩家是否已成功加入指定服务器。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Yggdrasil API"
|
||||
A[/yggdrasil/sessionserver]
|
||||
A --> B[GET /session/minecraft/profile/:uuid]
|
||||
A --> C[POST /session/minecraft/join]
|
||||
A --> D[GET /session/minecraft/hasJoined]
|
||||
end
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [routes.go](file://internal/handler/routes.go#L100-L105)
|
||||
|
||||
**Section sources**
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L105)
|
||||
|
||||
## 核心API功能详解
|
||||
|
||||
### JoinServer API
|
||||
`JoinServer` API是建立服务器会话的核心。当Minecraft客户端成功登录后,会调用此API来“加入”一个特定的服务器。
|
||||
|
||||
**功能流程:**
|
||||
1. **接收参数**:API接收`serverId`、`accessToken`和`selectedProfile`(玩家UUID)三个必需参数。
|
||||
2. **输入验证**:对`serverId`进行格式检查(长度不超过100字符,不包含`<>\"'&`等危险字符),并验证客户端IP地址格式。
|
||||
3. **令牌验证**:通过`accessToken`在数据库中查找对应的令牌记录,确保令牌有效。
|
||||
4. **配置文件匹配**:将`selectedProfile`(客户端提供的UUID)与令牌中关联的`ProfileId`进行比对,确保玩家使用的是正确的配置文件。
|
||||
5. **构建会话数据**:从数据库中获取该UUID对应的玩家档案,提取其用户名(`Name`)。
|
||||
6. **存储会话**:将`accessToken`、`userName`、`selectedProfile`和客户端IP地址构建成`SessionData`结构体,序列化后存入Redis。存储的键名为`Join_` + `serverId`,TTL设置为15分钟。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "Minecraft客户端"
|
||||
participant Handler as "yggdrasil_handler"
|
||||
participant Service as "yggdrasil_service"
|
||||
participant Redis as "Redis"
|
||||
participant DB as "数据库"
|
||||
Client->>Handler : POST /join (serverId, accessToken, selectedProfile)
|
||||
Handler->>Service : JoinServer(serverId, accessToken, selectedProfile, clientIP)
|
||||
Service->>DB : 根据accessToken查找Token
|
||||
DB-->>Service : Token信息
|
||||
Service->>Service : 验证selectedProfile与Token匹配
|
||||
Service->>DB : 根据UUID查找Profile
|
||||
DB-->>Service : Profile (含用户名)
|
||||
Service->>Service : 构建SessionData对象
|
||||
Service->>Service : 序列化SessionData
|
||||
Service->>Redis : Set(Join_serverId, marshaledData, 15min)
|
||||
Redis-->>Service : 成功
|
||||
Service-->>Handler : 成功
|
||||
Handler-->>Client : HTTP 204 No Content
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
|
||||
|
||||
### HasJoinedServer API
|
||||
`HasJoinedServer` API用于服务器端验证一个玩家是否已经通过了会话验证。
|
||||
|
||||
**功能流程:**
|
||||
1. **接收参数**:API接收`serverId`和`username`两个必需参数,以及可选的`ip`参数。
|
||||
2. **输入验证**:确保`serverId`和`username`不为空。
|
||||
3. **获取会话数据**:使用`Join_` + `serverId`作为键名,从Redis中获取之前存储的会话数据。
|
||||
4. **反序列化**:将获取到的JSON数据反序列化为`SessionData`结构体。
|
||||
5. **验证匹配**:
|
||||
- **用户名匹配**:比较会话数据中的`UserName`与请求中的`username`是否完全一致(区分大小写)。
|
||||
- **IP地址匹配**:如果请求中提供了`ip`参数,则会比较会话数据中的`IP`与请求的`ip`是否一致。
|
||||
6. **返回结果**:如果所有验证通过,则从数据库中获取该`username`对应的完整玩家档案(包括皮肤、披风等信息)并返回;否则返回验证失败。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Server as "Minecraft服务器"
|
||||
participant Handler as "yggdrasil_handler"
|
||||
participant Service as "yggdrasil_service"
|
||||
participant Redis as "Redis"
|
||||
participant DB as "数据库"
|
||||
Server->>Handler : GET /hasJoined?serverId=...&username=...&ip=...
|
||||
Handler->>Service : HasJoinedServer(serverId, username, ip)
|
||||
Service->>Redis : Get(Join_serverId)
|
||||
Redis-->>Service : marshaledData (或 nil)
|
||||
alt 会话不存在
|
||||
Service-->>Handler : 错误
|
||||
Handler-->>Server : HTTP 204 No Content
|
||||
else 会话存在
|
||||
Service->>Service : 反序列化为SessionData
|
||||
Service->>Service : 验证UserName == username
|
||||
Service->>Service : 验证IP (如果提供)
|
||||
alt 验证失败
|
||||
Service-->>Handler : 错误
|
||||
Handler-->>Server : HTTP 204 No Content
|
||||
else 验证成功
|
||||
Service->>DB : 根据username查找Profile
|
||||
DB-->>Service : Profile信息
|
||||
Service-->>Handler : Profile
|
||||
Handler-->>Server : HTTP 200 + Profile JSON
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
|
||||
|
||||
### GetProfileByUUID API
|
||||
`GetProfileByUUID` API提供了一种通过玩家UUID查询其公开档案信息的方式。
|
||||
|
||||
**功能流程:**
|
||||
1. **接收参数**:从URL路径中获取`:uuid`参数。
|
||||
2. **格式化UUID**:调用`utils.FormatUUID`函数,将可能存在的十六进制格式UUID转换为标准的带连字符格式。
|
||||
3. **查询档案**:调用`service.GetProfileByUUID`方法,根据格式化后的UUID在数据库中查找对应的`Profile`记录。
|
||||
4. **序列化响应**:将`Profile`模型转换为包含皮肤(Skin)和披风(Cape)URL的`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)
|
||||
@@ -1,309 +0,0 @@
|
||||
# 档案查询服务
|
||||
|
||||
<cite>
|
||||
**本文档引用文件**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [yggdrasil_handler_test.go](file://internal/handler/yggdrasil_handler_test.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [核心功能](#核心功能)
|
||||
3. [/api/profiles/minecraft 端点详解](#apiprofilesminecraft-端点详解)
|
||||
4. [/profile/:uuid 与 /api/profiles/minecraft 的区别](#profileuuid-与-apiprofilesminecraft-的区别)
|
||||
5. [皮肤与披风类型标识](#皮肤与披风类型标识)
|
||||
6. [请求与响应示例](#请求与响应示例)
|
||||
7. [应用场景](#应用场景)
|
||||
|
||||
## 简介
|
||||
|
||||
本服务为Minecraft Yggdrasil认证系统的一部分,提供档案(Profile)查询功能。核心功能包括根据用户名批量查询UUID和档案信息,以及根据UUID获取单个档案的完整详情。该服务支持Minecraft客户端在登录和聊天时加载玩家数据。
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L1-L667)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
|
||||
## 核心功能
|
||||
|
||||
档案查询服务主要提供两种查询方式:
|
||||
1. 批量查询:通过用户名列表获取对应的UUID和基础档案信息
|
||||
2. 详情查询:通过UUID获取单个档案的完整信息,包括皮肤和披风等纹理数据
|
||||
|
||||
这些功能通过Yggdrasil API的特定端点实现,支持Minecraft客户端的数据加载需求。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[客户端请求] --> B{请求类型}
|
||||
B --> |批量查询| C[/api/profiles/minecraft]
|
||||
B --> |详情查询| D[/profile/:uuid]
|
||||
C --> E[返回UUID和基础信息]
|
||||
D --> F[返回完整档案信息]
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [routes.go](file://internal/handler/routes.go#L108-L109)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L164-L195)
|
||||
|
||||
## /api/profiles/minecraft 端点详解
|
||||
|
||||
`/api/profiles/minecraft` 端点用于根据玩家用户名列表批量查询对应的UUID和公开档案信息。
|
||||
|
||||
### 功能说明
|
||||
|
||||
该端点允许客户端一次性查询多个玩家的档案信息,主要用于:
|
||||
- 游戏登录时预加载玩家数据
|
||||
- 聊天系统中显示玩家名称和头像
|
||||
- 服务器列表中显示在线玩家信息
|
||||
|
||||
### 实现流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 客户端
|
||||
participant Handler as yggdrasil_handler
|
||||
participant Service as profile_service
|
||||
participant Repository as profile_repository
|
||||
participant DB as 数据库
|
||||
Client->>Handler : POST /api/profiles/minecraft
|
||||
Handler->>Handler : 解析用户名数组
|
||||
Handler->>Service : GetProfilesDataByNames(names)
|
||||
Service->>Repository : GetProfilesByNames(names)
|
||||
Repository->>DB : SELECT * FROM profiles WHERE name IN (?)
|
||||
DB-->>Repository : 返回档案列表
|
||||
Repository-->>Service : 返回档案列表
|
||||
Service-->>Handler : 返回档案列表
|
||||
Handler-->>Client : 返回JSON响应
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L237-L243)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L129-L137)
|
||||
|
||||
### 请求参数
|
||||
|
||||
- **方法**: POST
|
||||
- **路径**: `/api/profiles/minecraft`
|
||||
- **内容类型**: `application/json`
|
||||
- **请求体**: 包含用户名字符串数组
|
||||
|
||||
### 错误处理
|
||||
|
||||
该端点会处理以下错误情况:
|
||||
- 请求体格式无效
|
||||
- 用户名数组为空
|
||||
- 数据库查询失败
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L237-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L129-L137)
|
||||
|
||||
## /profile/:uuid 与 /api/profiles/minecraft 的区别
|
||||
|
||||
这两个端点虽然都用于查询玩家档案,但功能和用途有明显区别:
|
||||
|
||||
### 功能对比
|
||||
|
||||
| 特性 | /profile/:uuid | /api/profiles/minecraft |
|
||||
|------|---------------|------------------------|
|
||||
| **查询方式** | 单个UUID查询 | 批量用户名查询 |
|
||||
| **主要用途** | 获取完整档案信息 | 批量映射用户名到UUID |
|
||||
| **返回数据** | 包含纹理数据的完整信息 | 基础档案信息 |
|
||||
| **使用场景** | 登录验证、档案详情展示 | 玩家列表、聊天显示 |
|
||||
|
||||
### 数据结构差异
|
||||
|
||||
`/profile/:uuid` 返回的数据包含完整的纹理信息:
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ProfileResponse {
|
||||
+string uuid
|
||||
+string name
|
||||
+ProfileTexturesData textures
|
||||
+bool is_active
|
||||
+time.Time last_used_at
|
||||
+time.Time created_at
|
||||
}
|
||||
class ProfileTexturesData {
|
||||
+ProfileTexture SKIN
|
||||
+ProfileTexture CAPE
|
||||
}
|
||||
class ProfileTexture {
|
||||
+string url
|
||||
+ProfileTextureMetadata metadata
|
||||
}
|
||||
class ProfileTextureMetadata {
|
||||
+string model
|
||||
}
|
||||
ProfileResponse --> ProfileTexturesData : "包含"
|
||||
ProfileTexturesData --> ProfileTexture : "包含"
|
||||
ProfileTexture --> ProfileTextureMetadata : "包含"
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [profile.go](file://internal/model/profile.go#L31-L57)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L164-L195)
|
||||
|
||||
而 `/api/profiles/minecraft` 返回的是基础档案信息,主要用于名称到UUID的映射。
|
||||
|
||||
**Section sources**
|
||||
- [profile.go](file://internal/model/profile.go#L31-L57)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L164-L195)
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
|
||||
|
||||
## 皮肤与披风类型标识
|
||||
|
||||
在档案系统中,皮肤和披风的类型通过特定常量进行标识。
|
||||
|
||||
### 常量定义
|
||||
|
||||
根据 `yggdrasil_handler_test.go` 中的测试代码,皮肤和披风类型的常量定义如下:
|
||||
|
||||
```go
|
||||
const (
|
||||
TextureTypeSkin = "SKIN"
|
||||
TextureTypeCape = "CAPE"
|
||||
)
|
||||
```
|
||||
|
||||
这些常量在多个地方被使用和测试,确保系统的一致性。
|
||||
|
||||
### 测试验证
|
||||
|
||||
`yggdrasil_handler_test.go` 文件中的测试函数验证了这些常量的正确性:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始测试常量]) --> TestSkin["验证TextureTypeSkin = 'SKIN'"]
|
||||
TestSkin --> TestCape["验证TextureTypeCape = 'CAPE'"]
|
||||
TestCape --> CheckSkin["检查Skin常量值"]
|
||||
CheckSkin --> CheckCape["检查Cape常量值"]
|
||||
CheckCape --> End{测试结果}
|
||||
End --> |通过| Success["测试成功"]
|
||||
End --> |失败| Failure["测试失败"]
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil_handler_test.go](file://internal/handler/yggdrasil_handler_test.go#L142-L156)
|
||||
- [texture.go](file://internal/model/texture.go#L10-L13)
|
||||
|
||||
### 数据模型
|
||||
|
||||
在 `texture.go` 文件中,这些类型被定义为枚举类型:
|
||||
|
||||
```go
|
||||
type TextureType string
|
||||
|
||||
const (
|
||||
TextureTypeSkin TextureType = "SKIN"
|
||||
TextureTypeCape TextureType = "CAPE"
|
||||
)
|
||||
```
|
||||
|
||||
这种设计确保了类型安全和代码可读性。
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler_test.go](file://internal/handler/yggdrasil_handler_test.go#L142-L156)
|
||||
- [texture.go](file://internal/model/texture.go#L8-L13)
|
||||
|
||||
## 请求与响应示例
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
POST /api/profiles/minecraft
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"Player1",
|
||||
"Player2",
|
||||
"Player3"
|
||||
]
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"userId": 1,
|
||||
"name": "Player1",
|
||||
"skinId": 1,
|
||||
"capeId": null,
|
||||
"isActive": true,
|
||||
"lastUsedAt": "2025-10-01T12:00:00Z",
|
||||
"createdAt": "2025-10-01T10:00:00Z",
|
||||
"updatedAt": "2025-10-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"userId": 2,
|
||||
"name": "Player2",
|
||||
"skinId": 2,
|
||||
"capeId": 3,
|
||||
"isActive": true,
|
||||
"lastUsedAt": "2025-10-01T11:00:00Z",
|
||||
"createdAt": "2025-09-30T09:00:00Z",
|
||||
"updatedAt": "2025-09-30T09:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
响应包含每个玩家的UUID、用户ID、名称、皮肤ID、披风ID等信息。
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L586)
|
||||
- [profile.go](file://internal/model/profile.go#L8-L24)
|
||||
|
||||
## 应用场景
|
||||
|
||||
### 游戏登录
|
||||
|
||||
当玩家登录游戏时,客户端会使用此服务来验证和获取玩家信息:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Minecraft客户端
|
||||
participant Auth as 认证服务器
|
||||
participant Profile as 档案服务
|
||||
Client->>Auth : 发送登录凭证
|
||||
Auth->>Profile : 查询玩家档案
|
||||
Profile->>Profile : 批量查询用户名对应的UUID
|
||||
Profile-->>Auth : 返回UUID和基础信息
|
||||
Auth->>Client : 返回登录结果和玩家信息
|
||||
```
|
||||
|
||||
**Diagram sources **
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
|
||||
- [routes.go](file://internal/handler/routes.go#L108-L109)
|
||||
|
||||
### 聊天显示
|
||||
|
||||
在聊天系统中,当玩家发送消息时,需要显示其名称和头像:
|
||||
|
||||
1. 客户端获取消息中的玩家用户名
|
||||
2. 调用 `/api/profiles/minecraft` 端点批量查询这些玩家的UUID
|
||||
3. 使用UUID获取玩家的皮肤信息并显示头像
|
||||
|
||||
### 服务器列表
|
||||
|
||||
在多人游戏服务器列表中,需要显示在线玩家的信息:
|
||||
|
||||
- 使用批量查询端点获取所有在线玩家的UUID
|
||||
- 根据UUID获取玩家的皮肤和披风信息
|
||||
- 在服务器列表中显示玩家头像和名称
|
||||
|
||||
这些应用场景都依赖于高效的批量查询能力,`/api/profiles/minecraft` 端点正是为此设计。
|
||||
|
||||
**Section sources**
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L553-L587)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L237-L243)
|
||||
@@ -1,464 +0,0 @@
|
||||
# 认证服务
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
|
||||
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go)
|
||||
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向Yggdrasil认证服务的API文档,聚焦/authserver路由下的以下端点:
|
||||
- POST /authenticate:用户凭据认证,返回访问令牌与可用角色信息
|
||||
- POST /validate:校验访问令牌有效性
|
||||
- POST /refresh:在令牌过期或需要更新时刷新令牌
|
||||
- POST /invalidate:撤销单个访问令牌
|
||||
- POST /signout:基于邮箱+密码登出,撤销该用户所有令牌
|
||||
|
||||
文档将说明各端点的HTTP方法、请求体结构、响应数据格式、错误码含义,并结合会话数据存储机制(Redis)与TTL设置(15分钟),解释authenticate如何验证用户名/邮箱与密码、validate如何检查令牌有效性、refresh如何在令牌过期后重新生成新令牌。同时给出与内部用户系统的映射逻辑(如通过GetYggdrasilPasswordById查询密码)。
|
||||
|
||||
## 项目结构
|
||||
Yggdrasil认证服务位于路由组“/api/v1/yggdrasil/authserver”,由处理器模块负责接收请求、调用服务层完成业务逻辑,并通过Redis与数据库进行会话与令牌持久化。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由层"
|
||||
R["routes.go<br/>注册/authserver路由"]
|
||||
H["yggdrasil_handler.go<br/>认证处理器"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["yggdrasil_service.go<br/>会话与加入服务器逻辑"]
|
||||
T["token_service.go<br/>令牌生成/验证/刷新/失效"]
|
||||
end
|
||||
subgraph "数据访问层"
|
||||
YR["yggdrasil_repository.go<br/>Yggdrasil密码查询"]
|
||||
end
|
||||
subgraph "基础设施"
|
||||
J["jwt.go<br/>JWT工具"]
|
||||
RC["redis.go<br/>Redis客户端"]
|
||||
RM["response.go<br/>通用响应结构"]
|
||||
end
|
||||
R --> H
|
||||
H --> S
|
||||
H --> T
|
||||
S --> YR
|
||||
T --> YR
|
||||
H --> J
|
||||
H --> RC
|
||||
H --> RM
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||||
|
||||
## 核心组件
|
||||
- 路由注册:在路由中将/authserver下的五个端点挂载到对应处理器函数。
|
||||
- 认证处理器:实现authenticate、validate、refresh、invalidate、signout等端点的请求解析、调用服务层、构造响应。
|
||||
- 令牌服务:封装令牌生成、验证、刷新、失效等逻辑,维护令牌表与清理策略。
|
||||
- 会话服务:封装JoinServer/HasJoinedServer,使用Redis存储会话数据,TTL为15分钟。
|
||||
- 数据访问:通过repository层访问数据库,如查询Yggdrasil密码。
|
||||
- 基础设施:JWT工具用于令牌签发与校验;Redis客户端用于会话数据持久化。
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
## 架构总览
|
||||
下图展示/authserver端点的请求-处理-响应流程,以及与服务层、Redis、数据库的关系。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "yggdrasil_handler.go"
|
||||
participant S as "yggdrasil_service.go"
|
||||
participant T as "token_service.go"
|
||||
participant YR as "yggdrasil_repository.go"
|
||||
participant DB as "数据库"
|
||||
participant RD as "Redis"
|
||||
rect rgb(255,255,255)
|
||||
Note over C,H : authenticate/validate/refresh/invalidate/signout
|
||||
end
|
||||
C->>H : POST /api/v1/yggdrasil/authserver/{endpoint}
|
||||
H->>DB : 读取/写入数据如用户、角色、令牌
|
||||
H->>RD : 读取/写入会话数据JoinServer/HasJoinedServer
|
||||
H->>T : 调用令牌相关服务生成/验证/刷新/失效
|
||||
H->>YR : 查询Yggdrasil密码凭据校验
|
||||
T->>DB : 操作token表创建/删除/查询
|
||||
S->>RD : Set/Get会话数据TTL=15分钟
|
||||
H-->>C : JSON响应含状态码与数据
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L425)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L202)
|
||||
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L175)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 端点:POST /authenticate
|
||||
- 方法与路径
|
||||
- 方法:POST
|
||||
- 路径:/api/v1/yggdrasil/authserver/authenticate
|
||||
- 请求体结构
|
||||
- agent:对象,描述客户端信息(通常包含名称与版本)
|
||||
- clientToken:字符串,客户端令牌(可选)
|
||||
- identifier:字符串,用户名或邮箱(必填)
|
||||
- password:字符串,密码(必填)
|
||||
- requestUser:布尔,是否返回用户属性(可选)
|
||||
- 响应数据
|
||||
- accessToken:字符串,访问令牌
|
||||
- clientToken:字符串,客户端令牌
|
||||
- availableProfiles:数组,可用角色列表(每个元素为序列化后的角色信息)
|
||||
- selectedProfile:对象,当前选定的角色(可选)
|
||||
- user:对象,当requestUser=true时返回(包含id与properties)
|
||||
- 错误码
|
||||
- 400:请求参数错误
|
||||
- 403:用户名不存在或密码错误
|
||||
- 500:服务器内部错误
|
||||
- 处理流程要点
|
||||
- 根据identifier判断是邮箱还是用户名,分别查询用户或角色
|
||||
- 通过repository查询Yggdrasil密码并与请求密码比对
|
||||
- 生成新令牌(包含accessToken、clientToken),并返回可用角色列表
|
||||
- 如requestUser为true,附加用户属性
|
||||
- 与内部用户系统映射
|
||||
- 通过GetYggdrasilPasswordById查询Yggdrasil密码并与请求密码比对
|
||||
- 通过GetUserIDByEmail或GetProfileByProfileName获取用户ID
|
||||
- 通过GetProfileByUserId获取角色列表,自动选择唯一角色
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "Authenticate()"
|
||||
participant DB as "数据库"
|
||||
participant YR as "yggdrasil_repository.go"
|
||||
participant T as "token_service.go"
|
||||
C->>H : JSON请求identifier/password等
|
||||
H->>DB : 根据identifier定位用户/角色
|
||||
H->>YR : GetYggdrasilPasswordById(userId)
|
||||
YR-->>H : 返回Yggdrasil密码
|
||||
H->>H : 校验密码
|
||||
H->>T : NewToken(userId, UUID, clientToken)
|
||||
T-->>H : 返回accessToken/clientToken/availableProfiles
|
||||
H-->>C : 200 JSONaccessToken/clientToken/availableProfiles/selectedProfile/user
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L246)
|
||||
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L79)
|
||||
- [token_service.go](file://internal/service/token_service.go#L24-L79)
|
||||
|
||||
章节来源
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L156-L246)
|
||||
- [yggdrasil_repository.go](file://internal/repository/yggdrasil_repository.go#L1-L17)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L79)
|
||||
- [token_service.go](file://internal/service/token_service.go#L24-L79)
|
||||
|
||||
### 端点:POST /validate
|
||||
- 方法与路径
|
||||
- 方法:POST
|
||||
- 路径:/api/v1/yggdrasil/authserver/validate
|
||||
- 请求体结构
|
||||
- accessToken:字符串,访问令牌(必填)
|
||||
- clientToken:字符串,客户端令牌(可选)
|
||||
- 响应数据
|
||||
- 当令牌有效:204 No Content(无body)
|
||||
- 当令牌无效:403 Forbidden(valid=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)
|
||||
|
||||
### 会话数据存储与TTL(JoinServer/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层访问数据库
|
||||
- 会话服务到Redis:JoinServer/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/Set,TTL为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)
|
||||
@@ -1,284 +0,0 @@
|
||||
# 分页处理机制
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [texture_service_test.go](file://internal/service/texture_service_test.go)
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
本文件围绕材质(Texture)相关API的分页处理机制进行系统化梳理,重点基于测试用例与实现代码,阐明以下内容:
|
||||
- 分页参数的边界处理规则:page小于1时自动设为1;pageSize小于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_pages:total_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上限,系统在大多数场景下能够稳定运行。针对高频列表页,建议进一步引入缓存与按需加载策略,以应对更大规模的数据与更高的并发需求。
|
||||
@@ -1,431 +0,0 @@
|
||||
# 材质API
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [upload_service.go](file://internal/service/upload_service.go)
|
||||
- [minio.go](file://pkg/storage/minio.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [texture_service_test.go](file://internal/service/texture_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向“材质管理API”的使用者与维护者,系统性梳理材质的搜索、上传、创建、更新、删除、收藏及分页查询能力。基于路由组 /api/v1/texture 的公开与认证两类访问模式,详细说明各端点的行为、参数、响应与错误处理,并深入解析生成上传URL的流程、材质元数据创建、分页查询策略以及收藏功能的实现细节。同时解释材质与用户的关系及权限控制机制,帮助读者快速上手并正确集成。
|
||||
|
||||
## 项目结构
|
||||
- 路由注册集中在路由处理器中,按组划分公开与认证两类接口。
|
||||
- 处理器负责参数校验、鉴权、调用服务层并返回统一响应。
|
||||
- 服务层封装业务规则(如权限校验、分页边界、收藏增删计数)。
|
||||
- 仓储层负责数据库查询与写入(含软删除、计数更新、收藏关联)。
|
||||
- 模型层定义材质、收藏、下载日志等实体及其索引。
|
||||
- 上传服务与存储客户端负责生成预签名上传URL与对象命名策略。
|
||||
- 中间件提供JWT认证与可选认证能力。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "HTTP层"
|
||||
R["路由注册<br/>routes.go"]
|
||||
H["处理器<br/>texture_handler.go"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["服务<br/>texture_service.go"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
REPO["仓储<br/>texture_repository.go"]
|
||||
end
|
||||
subgraph "模型层"
|
||||
M["模型<br/>texture.go"]
|
||||
end
|
||||
subgraph "上传与存储"
|
||||
U["上传服务<br/>upload_service.go"]
|
||||
ST["存储客户端<br/>minio.go"]
|
||||
end
|
||||
subgraph "鉴权"
|
||||
A["认证中间件<br/>auth.go"]
|
||||
end
|
||||
R --> H
|
||||
H --> A
|
||||
H --> S
|
||||
S --> REPO
|
||||
REPO --> M
|
||||
H --> U
|
||||
U --> ST
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
|
||||
## 核心组件
|
||||
- 路由与鉴权
|
||||
- 公开端点:GET /api/v1/texture、GET /api/v1/texture/{id}
|
||||
- 认证端点:POST /api/v1/texture/upload-url、POST /api/v1/texture、PUT /api/v1/texture/{id}、DELETE /api/v1/texture/{id}、POST /api/v1/texture/{id}/favorite、GET /api/v1/texture/my、GET /api/v1/texture/favorites
|
||||
- 认证中间件要求 Authorization: Bearer <token>,通过后将用户信息注入上下文
|
||||
- 数据模型
|
||||
- 材质实体包含上传者ID、名称、描述、类型、URL、哈希、大小、公开状态、下载/收藏计数、是否细臂、状态、时间戳等;并关联上传者
|
||||
- 收藏关联表 user_texture_favorites 记录用户与材质的收藏关系
|
||||
- 下载日志表 texture_download_logs 记录下载行为
|
||||
- 上传与存储
|
||||
- 生成预签名POST URL,限定文件类型、大小范围与过期时间,对象路径按用户与材质类型组织
|
||||
- 服务与仓储
|
||||
- 提供搜索、分页、权限校验、收藏切换、软删除、计数更新等业务逻辑
|
||||
- 仓储实现分页查询、总数统计、软删除、收藏增删与计数更新
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
|
||||
## 架构总览
|
||||
下图展示从HTTP请求到数据库与存储的关键交互路径,涵盖公开搜索、认证上传与创建、权限校验、收藏与分页等。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "路由<br/>routes.go"
|
||||
participant H as "处理器<br/>texture_handler.go"
|
||||
participant A as "认证中间件<br/>auth.go"
|
||||
participant S as "服务<br/>texture_service.go"
|
||||
participant REPO as "仓储<br/>texture_repository.go"
|
||||
participant M as "模型<br/>texture.go"
|
||||
participant U as "上传服务<br/>upload_service.go"
|
||||
participant ST as "存储客户端<br/>minio.go"
|
||||
C->>R : 请求 /api/v1/texture/search
|
||||
R->>H : 调用 SearchTextures
|
||||
H->>S : 调用 SearchTextures(db, keyword, type, publicOnly, page, pageSize)
|
||||
S->>REPO : 查询并统计总数
|
||||
REPO-->>S : 返回列表与总数
|
||||
S-->>H : 返回结果
|
||||
H-->>C : 200 + 分页数据
|
||||
C->>R : 请求 /api/v1/texture/upload-url (认证)
|
||||
R->>A : 鉴权
|
||||
A-->>R : 注入用户ID
|
||||
R->>H : 调用 GenerateTextureUploadURL
|
||||
H->>U : 生成预签名POST URL
|
||||
U->>ST : 生成POST策略与URL
|
||||
ST-->>U : 返回PostURL、FormData、FileURL
|
||||
U-->>H : 返回结果
|
||||
H-->>C : 200 + 上传URL与过期时间
|
||||
C->>R : 请求 /api/v1/texture (认证)
|
||||
R->>A : 鉴权
|
||||
A-->>R : 注入用户ID
|
||||
R->>H : 调用 CreateTexture
|
||||
H->>S : 检查上传配额与创建材质
|
||||
S->>REPO : 写入材质记录
|
||||
REPO-->>S : 返回新记录
|
||||
S-->>H : 返回材质信息
|
||||
H-->>C : 200 + 材质详情
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L121)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 路由与访问模式
|
||||
- 公开访问
|
||||
- GET /api/v1/texture:搜索材质(关键词、类型、公开筛选、分页)
|
||||
- GET /api/v1/texture/{id}:获取材质详情
|
||||
- 认证访问
|
||||
- POST /api/v1/texture/upload-url:生成材质上传URL(预签名POST)
|
||||
- POST /api/v1/texture:创建材质记录(文件已上传至存储)
|
||||
- PUT /api/v1/texture/{id}:更新材质(仅上传者可操作)
|
||||
- DELETE /api/v1/texture/{id}:删除材质(软删除,仅上传者可操作)
|
||||
- POST /api/v1/texture/{id}/favorite:切换收藏
|
||||
- GET /api/v1/texture/my:获取当前用户上传的材质列表(分页)
|
||||
- GET /api/v1/texture/favorites:获取当前用户收藏的材质列表(分页)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
|
||||
### 生成上传URL流程
|
||||
- 客户端向 POST /api/v1/texture/upload-url 发起请求,携带文件名与材质类型(SKIN/CAPE)
|
||||
- 处理器校验请求体、获取用户ID并调用上传服务
|
||||
- 上传服务:
|
||||
- 校验文件名扩展名与类型
|
||||
- 选择对应上传配置(大小范围、过期时间)
|
||||
- 解析存储桶名称(textures)
|
||||
- 生成对象名:user_{userID}/{textureTypeFolder}/{timestamp}_{originalFileName}
|
||||
- 生成预签名POST策略(含内容长度范围、过期时间),返回PostURL、FormData与最终访问URL
|
||||
- 客户端使用返回的PostURL与FormData直传到对象存储,成功后调用 POST /api/v1/texture 创建材质记录
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> Bind["绑定请求体<br/>文件名/类型"]
|
||||
Bind --> Validate["校验文件名与类型"]
|
||||
Validate --> Config["加载上传配置"]
|
||||
Config --> Bucket["解析存储桶名称"]
|
||||
Bucket --> ObjName["生成对象名<br/>user_{id}/{type}/{ts}_{fileName}"]
|
||||
ObjName --> Policy["生成预签名POST策略"]
|
||||
Policy --> Result["返回PostURL/FormData/FileURL"]
|
||||
Result --> End(["结束"])
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L121)
|
||||
|
||||
章节来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L121)
|
||||
|
||||
### 材质元数据创建
|
||||
- 客户端上传完成后,向 POST /api/v1/texture 提交材质元数据(名称、描述、类型、URL、哈希、大小、公开状态、是否细臂)
|
||||
- 处理器:
|
||||
- 校验请求体
|
||||
- 检查用户上传配额(默认最大100条)
|
||||
- 调用服务层创建材质记录(校验用户存在、去重哈希、转换类型、初始化默认值)
|
||||
- 写入数据库并返回材质信息
|
||||
- 服务层:
|
||||
- 校验用户存在
|
||||
- 哈希去重检查
|
||||
- 类型转换(SKIN/CAPE)
|
||||
- 初始化状态、下载/收藏计数为0
|
||||
- 仓储层:
|
||||
- 插入记录
|
||||
|
||||
章节来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L9-L13)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L35)
|
||||
|
||||
### 搜索与分页查询
|
||||
- GET /api/v1/texture 支持:
|
||||
- keyword:关键词(名称/描述模糊匹配)
|
||||
- type:SKIN/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_type(SKIN/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)
|
||||
@@ -1,145 +0,0 @@
|
||||
# 材质上传与管理
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [upload_service.go](file://internal/service/upload_service.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [上传流程与元数据创建](#上传流程与元数据创建)
|
||||
3. [上传限制检查机制](#上传限制检查机制)
|
||||
4. [材质更新API权限控制](#材质更新api权限控制)
|
||||
5. [字段更新策略](#字段更新策略)
|
||||
6. [错误处理场景](#错误处理场景)
|
||||
|
||||
## 简介
|
||||
本系统提供完整的材质上传与管理功能,支持用户上传皮肤(SKIN)和披风(CAPE)材质。系统通过预签名URL实现安全的文件上传,确保只有经过身份验证的用户才能上传和管理自己的材质。材质元数据存储在数据库中,包含名称、描述、类型、哈希值等信息。系统实现了严格的权限控制,确保只有上传者才能修改或删除自己的材质。同时,系统还提供了收藏、下载统计等功能,增强了用户体验。
|
||||
|
||||
## 上传流程与元数据创建
|
||||
材质上传流程分为两个阶段:生成上传URL和创建材质元数据。首先,用户通过调用`/api/v1/texture/upload-url`接口获取预签名的上传URL。该接口验证用户身份、文件名和材质类型后,生成一个临时的上传链接,允许用户直接上传文件到存储服务。上传完成后,用户调用`/api/v1/texture`接口创建材质元数据。系统会验证用户是否存在、材质哈希是否重复,并创建相应的数据库记录。材质元数据包括上传者ID、名称、描述、类型、URL、哈希值、大小、公开状态等信息。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 用户 as 用户
|
||||
participant 处理器 as texture_handler
|
||||
participant 服务 as upload_service
|
||||
participant 存储 as 存储服务
|
||||
用户->>处理器 : POST /api/v1/texture/upload-url
|
||||
处理器->>服务 : GenerateTextureUploadURL()
|
||||
服务->>服务 : 验证文件名和类型
|
||||
服务->>存储 : 生成预签名POST URL
|
||||
存储-->>服务 : 返回预签名URL
|
||||
服务-->>处理器 : 返回URL
|
||||
处理器-->>用户 : 返回上传URL
|
||||
用户->>存储 : 使用预签名URL上传文件
|
||||
存储-->>用户 : 上传成功
|
||||
用户->>处理器 : POST /api/v1/texture
|
||||
处理器->>服务 : CreateTexture()
|
||||
服务->>服务 : 验证用户和哈希
|
||||
服务->>repository : CreateTexture()
|
||||
repository-->>服务 : 创建成功
|
||||
服务-->>处理器 : 返回材质信息
|
||||
处理器-->>用户 : 返回创建结果
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L63)
|
||||
- [routes.go](file://internal/handler/routes.go#L53-L54)
|
||||
|
||||
**本节来源**
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L63)
|
||||
|
||||
## 上传限制检查机制
|
||||
系统通过`CheckTextureUploadLimit`函数实现用户上传数量限制。该函数接收数据库连接、上传者ID和最大允许上传数量作为参数。首先,函数调用`CountTexturesByUploaderID`从数据库中统计指定用户已上传的材质数量。然后,将统计结果与最大限制进行比较。如果当前上传数量大于或等于最大限制,函数返回错误信息,提示用户已达到最大上传数量限制。否则,函数返回nil,表示可以继续上传。系统配置中定义了每个用户的最大材质数量限制,默认值为100。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> Count["调用CountTexturesByUploaderID<br/>统计用户上传数量"]
|
||||
Count --> Compare{"当前数量 >= 最大限制?"}
|
||||
Compare --> |是| ReturnError["返回错误信息:<br/>已达到最大上传数量限制"]
|
||||
Compare --> |否| ReturnSuccess["返回nil,允许上传"]
|
||||
ReturnError --> End([结束])
|
||||
ReturnSuccess --> End
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L223-L231)
|
||||
|
||||
**本节来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
|
||||
|
||||
## 材质更新API权限控制
|
||||
材质更新API实现了严格的权限控制,确保只有上传者才能修改自己的材质。当用户尝试更新材质时,系统首先通过`FindTextureByID`从数据库中获取材质信息。然后,比较请求中的上传者ID与材质记录中的`UploaderID`。如果两者不匹配,系统返回"无权修改此材质"的错误信息。这种基于上传者ID的权限检查机制有效防止了未经授权的修改操作。权限检查在`UpdateTexture`服务函数中实现,是更新操作的第一步,确保在进行任何数据修改之前完成身份验证。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> GetTexture["调用FindTextureByID<br/>获取材质信息"]
|
||||
GetTexture --> CheckExist{"材质存在?"}
|
||||
CheckExist --> |否| ReturnError1["返回错误:<br/>材质不存在"]
|
||||
CheckExist --> |是| CheckPermission{"UploaderID匹配?"}
|
||||
CheckPermission --> |否| ReturnError2["返回错误:<br/>无权修改此材质"]
|
||||
CheckPermission --> |是| UpdateFields["更新指定字段"]
|
||||
UpdateFields --> ReturnSuccess["返回更新后的材质"]
|
||||
ReturnError1 --> End([结束])
|
||||
ReturnError2 --> End
|
||||
ReturnSuccess --> End
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L16-L27)
|
||||
|
||||
**本节来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
|
||||
## 字段更新策略
|
||||
系统采用灵活的字段更新策略,仅更新客户端提供的字段。当更新材质时,系统创建一个`updates`映射来存储需要更新的字段。如果提供了名称且不为空,将其添加到`updates`映射中;如果提供了描述且不为空,也将其添加到映射中;如果提供了公开状态(`isPublic`),同样将其添加到映射中。只有当`updates`映射不为空时,系统才会调用`UpdateTextureFields`执行数据库更新操作。这种策略避免了不必要的数据库写入,提高了性能,并确保未提供的字段保持原值不变。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> InitMap["初始化updates映射"]
|
||||
InitMap --> CheckName{"名称提供且不为空?"}
|
||||
CheckName --> |是| AddName["添加name到updates映射"]
|
||||
CheckName --> |否| CheckDesc
|
||||
AddName --> CheckDesc
|
||||
CheckDesc --> {"描述提供且不为空?"}
|
||||
CheckDesc --> |是| AddDesc["添加description到updates映射"]
|
||||
CheckDesc --> |否| CheckPublic
|
||||
AddDesc --> CheckPublic
|
||||
CheckPublic --> {"公开状态提供?"}
|
||||
CheckPublic --> |是| AddPublic["添加is_public到updates映射"]
|
||||
CheckPublic --> |否| CheckUpdates
|
||||
AddPublic --> CheckUpdates
|
||||
CheckUpdates --> {"updates映射为空?"}
|
||||
CheckUpdates --> |是| ReturnOriginal["返回原材质"]
|
||||
CheckUpdates --> |否| UpdateDB["调用UpdateTextureFields<br/>更新数据库"]
|
||||
UpdateDB --> ReturnUpdated["返回更新后的材质"]
|
||||
ReturnOriginal --> End([结束])
|
||||
ReturnUpdated --> End
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
|
||||
|
||||
**本节来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
|
||||
## 错误处理场景
|
||||
系统在材质上传与管理过程中实现了全面的错误处理机制。当用户达到上传上限时,`CheckTextureUploadLimit`函数返回"已达到最大上传数量限制"的错误信息。当用户尝试修改不属于自己的材质时,`UpdateTexture`函数返回"无权修改此材质"的错误信息。其他常见错误包括:用户不存在、材质已存在、无效的材质类型、文件名为空、不支持的文件格式等。所有错误都通过标准的错误响应格式返回给客户端,包含错误代码和描述信息,便于前端进行相应的错误处理和用户提示。
|
||||
|
||||
**本节来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L120-L127)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L19-L21)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L28-L30)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L40-L41)
|
||||
@@ -1,327 +0,0 @@
|
||||
# 材质上传流程
|
||||
|
||||
<cite>
|
||||
**本文档引用文件**
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [流程概述](#流程概述)
|
||||
2. [API接口说明](#api接口说明)
|
||||
3. [生成预签名上传URL](#生成预签名上传url)
|
||||
4. [创建材质记录](#创建材质记录)
|
||||
5. [错误处理机制](#错误处理机制)
|
||||
6. [流程时序图](#流程时序图)
|
||||
|
||||
## 流程概述
|
||||
|
||||
材质上传流程采用分步式设计,包含两个核心API接口:`GenerateTextureUploadURL`和`CreateTexture`。该流程遵循安全最佳实践,通过预签名URL机制实现文件上传,确保文件在上传到存储系统后才在数据库中创建相应记录。
|
||||
|
||||
整个流程分为两个阶段:
|
||||
1. **准备阶段**:客户端调用`GenerateTextureUploadURL`接口,服务器验证权限后返回预签名上传URL和表单数据
|
||||
2. **完成阶段**:客户端使用返回的凭证上传文件后,调用`CreateTexture`接口创建材质元数据记录
|
||||
|
||||
这种设计确保了只有成功上传的文件才会在系统中创建记录,避免了数据库中出现孤立的记录。
|
||||
|
||||
## API接口说明
|
||||
|
||||
材质上传相关的API接口定义在路由配置中,主要包含以下两个核心接口:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[客户端] --> B[GenerateTextureUploadURL]
|
||||
A --> C[CreateTexture]
|
||||
B --> D[返回预签名URL和表单数据]
|
||||
C --> E[创建材质记录]
|
||||
```
|
||||
|
||||
**接口来源**
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L53-L54)
|
||||
|
||||
## 生成预签名上传URL
|
||||
|
||||
`GenerateTextureUploadURL`接口负责生成临时的文件上传凭证,使客户端能够直接上传文件到对象存储系统。
|
||||
|
||||
### 接口实现
|
||||
|
||||
该接口的处理流程如下:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> AuthCheck["验证用户身份"]
|
||||
AuthCheck --> ValidateInput["验证请求参数"]
|
||||
ValidateInput --> CheckFileName["验证文件名"]
|
||||
CheckFileName --> CheckTextureType["验证材质类型"]
|
||||
CheckTextureType --> GetStorageConfig["获取存储配置"]
|
||||
GetStorageConfig --> GenerateObjectName["生成对象名称"]
|
||||
GenerateObjectName --> GeneratePresignedURL["生成预签名POST URL"]
|
||||
GeneratePresignedURL --> ReturnResult["返回PostURL和FormData"]
|
||||
ReturnResult --> End([结束])
|
||||
```
|
||||
|
||||
**代码来源**
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L28-L83)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
|
||||
### 请求参数
|
||||
|
||||
请求体包含以下字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| fileName | string | 上传的文件名 |
|
||||
| textureType | string | 材质类型(SKIN或CAPE) |
|
||||
|
||||
### 响应格式
|
||||
|
||||
成功响应包含预签名上传所需的所有信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"postURL": "https://storage.example.com/textures",
|
||||
"formData": {
|
||||
"key": "user_1/skin/20231201120000_texture.png",
|
||||
"policy": "base64-encoded-policy",
|
||||
"signature": "request-signature",
|
||||
"AWSAccessKeyId": "access-key-id"
|
||||
},
|
||||
"textureURL": "https://storage.example.com/textures/user_1/skin/20231201120000_texture.png",
|
||||
"expiresIn": 900
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实现细节
|
||||
|
||||
1. **文件名验证**:检查文件扩展名是否为`.png`,确保只允许上传PNG格式的材质文件
|
||||
2. **类型验证**:确认材质类型为`SKIN`或`CAPE`之一
|
||||
3. **对象名称生成**:采用`user_{userId}/{textureType}/timestamp_{originalFileName}`的格式,确保文件路径的唯一性
|
||||
4. **预签名URL生成**:调用存储模块的`GeneratePresignedPostURL`方法创建临时上传凭证
|
||||
|
||||
**存储实现来源**
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L120)
|
||||
|
||||
## 创建材质记录
|
||||
|
||||
`CreateTexture`接口在文件上传完成后创建材质的元数据记录,将文件与用户关联起来。
|
||||
|
||||
### 接口实现
|
||||
|
||||
该接口的处理流程包含多个验证步骤:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> AuthCheck["验证用户身份"]
|
||||
AuthCheck --> ValidateInput["验证请求参数"]
|
||||
ValidateInput --> CheckLimit["检查上传数量限制"]
|
||||
CheckLimit --> CheckHash["检查文件Hash是否重复"]
|
||||
CheckHash --> ValidateUser["验证用户存在"]
|
||||
ValidateUser --> ConvertType["转换材质类型"]
|
||||
ConvertType --> CreateRecord["创建材质记录"]
|
||||
CreateRecord --> ReturnResult["返回材质信息"]
|
||||
ReturnResult --> End([结束])
|
||||
style CheckLimit fill:#f9f,stroke:#333
|
||||
style CheckHash fill:#f9f,stroke:#333
|
||||
```
|
||||
|
||||
**代码来源**
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L95-L172)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
|
||||
### 请求参数
|
||||
|
||||
请求体包含材质的元数据信息:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| name | string | 材质名称 |
|
||||
| description | string | 描述信息 |
|
||||
| type | string | 材质类型(SKIN或CAPE) |
|
||||
| url | string | 文件访问URL |
|
||||
| hash | string | 文件SHA-256哈希值 |
|
||||
| size | int | 文件大小(字节) |
|
||||
| isPublic | boolean | 是否公开 |
|
||||
| isSlim | boolean | 是否为细身模型 |
|
||||
|
||||
### 响应格式
|
||||
|
||||
成功创建材质后返回材质的详细信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"uploaderID": 1,
|
||||
"name": "My Skin",
|
||||
"description": "A custom skin",
|
||||
"type": "SKIN",
|
||||
"url": "https://storage.example.com/textures/user_1/skin/20231201120000_texture.png",
|
||||
"hash": "sha256-hash-value",
|
||||
"size": 10240,
|
||||
"isPublic": true,
|
||||
"downloadCount": 0,
|
||||
"favoriteCount": 0,
|
||||
"isSlim": false,
|
||||
"status": 1,
|
||||
"createdAt": "2023-12-01T12:00:00Z",
|
||||
"updatedAt": "2023-12-01T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 核心验证逻辑
|
||||
|
||||
#### 上传数量限制检查
|
||||
|
||||
系统通过`CheckTextureUploadLimit`函数检查用户是否达到上传上限:
|
||||
|
||||
```go
|
||||
func CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) error {
|
||||
count, err := repository.CountTexturesByUploaderID(uploaderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count >= int64(maxTextures) {
|
||||
return fmt.Errorf("已达到最大上传数量限制(%d)", maxTextures)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**代码来源**
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L223-L231)
|
||||
|
||||
#### 重复上传防止
|
||||
|
||||
通过文件的SHA-256哈希值防止重复上传相同内容的材质:
|
||||
|
||||
```go
|
||||
// 检查Hash是否已存在
|
||||
existingTexture, err := repository.FindTextureByHash(hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingTexture != nil {
|
||||
return nil, errors.New("该材质已存在")
|
||||
}
|
||||
```
|
||||
|
||||
**代码来源**
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L30)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L29-L41)
|
||||
|
||||
## 错误处理机制
|
||||
|
||||
系统针对不同场景提供了详细的错误响应,帮助客户端正确处理各种异常情况。
|
||||
|
||||
### 错误码定义
|
||||
|
||||
| 错误码 | HTTP状态码 | 说明 |
|
||||
|--------|------------|------|
|
||||
| 400 | 400 | 请求参数错误 |
|
||||
| 401 | 401 | 未授权访问 |
|
||||
| 403 | 403 | 无权操作 |
|
||||
| 404 | 404 | 资源不存在 |
|
||||
|
||||
### 常见错误场景
|
||||
|
||||
#### 上传数量达到上限
|
||||
|
||||
当用户上传的材质数量达到系统限制时,返回以下错误:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "已达到最大上传数量限制(100)",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 权限不足
|
||||
|
||||
未登录用户或非上传者尝试操作时返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "Unauthorized",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 文件Hash冲突
|
||||
|
||||
上传已存在的材质文件时返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "该材质已存在",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 无效的材质类型
|
||||
|
||||
指定不支持的材质类型时返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "无效的材质类型: INVALID",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 流程时序图
|
||||
|
||||
以下是完整的材质上传流程时序图:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Handler as "处理器"
|
||||
participant Service as "服务层"
|
||||
participant Storage as "存储模块"
|
||||
participant DB as "数据库"
|
||||
Client->>Handler : 调用GenerateTextureUploadURL
|
||||
Handler->>Service : 验证参数并调用GenerateTextureUploadURL
|
||||
Service->>Service : 验证文件名和类型
|
||||
Service->>Storage : 获取存储桶名称
|
||||
Storage-->>Service : 返回存储桶名称
|
||||
Service->>Storage : 生成预签名POST URL
|
||||
Storage-->>Service : 返回PostURL和FormData
|
||||
Service-->>Handler : 返回结果
|
||||
Handler-->>Client : 返回预签名上传信息
|
||||
Client->>Storage : 使用PostURL上传文件
|
||||
Storage-->>Client : 上传成功响应
|
||||
Client->>Handler : 调用CreateTexture
|
||||
Handler->>Service : 验证参数并调用CreateTexture
|
||||
Service->>DB : 检查用户上传数量限制
|
||||
DB-->>Service : 返回数量统计
|
||||
Service->>DB : 检查文件Hash是否重复
|
||||
DB-->>Service : 返回查询结果
|
||||
Service->>DB : 创建材质记录
|
||||
DB-->>Service : 创建成功
|
||||
Service-->>Handler : 返回材质信息
|
||||
Handler-->>Client : 返回创建结果
|
||||
```
|
||||
|
||||
**时序图来源**
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
|
||||
@@ -1,460 +0,0 @@
|
||||
# 材质管理操作
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [manager.go](file://pkg/database/manager.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向后端开发者与运维人员,系统化梳理材质管理模块的增删改查与收藏能力,重点覆盖以下内容:
|
||||
- UpdateTexture 接口的权限控制与字段级更新策略
|
||||
- DeleteTexture 接口的软删除与权限校验
|
||||
- ToggleFavorite 接口的收藏切换与 FavoriteCount 同步
|
||||
- GetTexture、SearchTextures、GetUserTextures 的使用方式、分页与过滤规则
|
||||
- 请求/响应结构、认证要求与常见错误码说明
|
||||
|
||||
## 项目结构
|
||||
材质管理相关代码采用典型的分层架构:
|
||||
- 路由层:注册 API 路由与鉴权中间件
|
||||
- 处理层:HTTP 控制器,负责参数解析、鉴权、调用服务层并返回统一响应
|
||||
- 服务层:业务逻辑编排,包含权限校验、字段级更新、软删除、收藏切换等
|
||||
- 仓储层:数据库访问封装,提供查询、更新、计数等方法
|
||||
- 模型层:实体定义与数据库映射
|
||||
- 类型与响应:请求/响应结构体与统一响应模型
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由层"
|
||||
R["routes.go<br/>注册纹理相关路由"]
|
||||
end
|
||||
subgraph "中间件"
|
||||
M["auth.go<br/>JWT鉴权中间件"]
|
||||
end
|
||||
subgraph "处理层"
|
||||
H["texture_handler.go<br/>控制器:更新/删除/收藏/查询"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["texture_service.go<br/>业务逻辑:权限/字段更新/软删除/收藏切换"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
REPO["texture_repository.go<br/>数据库访问:查询/更新/计数"]
|
||||
end
|
||||
subgraph "模型层"
|
||||
MOD["texture.go<br/>实体:Texture/UserTextureFavorite"]
|
||||
end
|
||||
subgraph "类型与响应"
|
||||
T["common.go<br/>请求/响应结构体"]
|
||||
RESP["response.go<br/>统一响应模型"]
|
||||
end
|
||||
DB["manager.go<br/>数据库初始化/迁移"]
|
||||
R --> M --> H --> S --> REPO --> DB
|
||||
H --> T
|
||||
H --> RESP
|
||||
S --> MOD
|
||||
REPO --> MOD
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
|
||||
- [texture.go](file://internal/model/texture.go#L15-L77)
|
||||
- [common.go](file://internal/types/common.go#L86-L152)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
- [manager.go](file://pkg/database/manager.go#L52-L99)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
|
||||
- [texture.go](file://internal/model/texture.go#L15-L77)
|
||||
- [common.go](file://internal/types/common.go#L86-L152)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
- [manager.go](file://pkg/database/manager.go#L52-L99)
|
||||
|
||||
## 核心组件
|
||||
- 路由与鉴权
|
||||
- 纹理路由组在 v1 下注册,其中部分接口需 JWT 鉴权;公开接口无需认证
|
||||
- 鉴权中间件从 Authorization 头提取 Bearer Token 并校验,通过后将用户信息注入上下文
|
||||
- 处理器
|
||||
- 提供 UpdateTexture、DeleteTexture、ToggleFavorite、GetTexture、SearchTextures、GetUserTextures 等接口
|
||||
- 参数绑定、分页默认值设置、统一响应封装
|
||||
- 服务层
|
||||
- UpdateTexture 字段级更新策略:仅当字段非空/非零时才更新
|
||||
- DeleteTexture 软删除:将 Status 设为删除态,不影响数据完整性
|
||||
- ToggleFavorite:根据收藏状态切换,同步更新 FavoriteCount
|
||||
- 查询接口:分页参数校验与默认值处理
|
||||
- 仓储层
|
||||
- 提供按条件查询、分页、计数、字段更新、软删除、收藏相关 CRUD 等
|
||||
- 模型层
|
||||
- Texture:包含上传者、名称、描述、类型、URL、哈希、大小、公开状态、下载/收藏计数、状态等
|
||||
- UserTextureFavorite:收藏关联表
|
||||
- 类型与响应
|
||||
- UpdateTextureRequest、CreateTextureRequest、TextureInfo 等结构体
|
||||
- 统一响应模型与常用状态码
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
|
||||
- [texture.go](file://internal/model/texture.go#L15-L77)
|
||||
- [common.go](file://internal/types/common.go#L86-L152)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
## 架构总览
|
||||
下图展示“更新材质”接口的端到端调用链路,体现鉴权、参数校验、权限检查、字段级更新与返回结果的完整流程。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "路由(routes.go)"
|
||||
participant MW as "鉴权中间件(auth.go)"
|
||||
participant H as "控制器(texture_handler.go)"
|
||||
participant S as "服务层(texture_service.go)"
|
||||
participant REPO as "仓储层(texture_repository.go)"
|
||||
participant DB as "数据库"
|
||||
C->>R : "PUT /api/v1/texture/ : id"
|
||||
R->>MW : "进入AuthMiddleware()"
|
||||
MW-->>R : "校验通过,注入user_id"
|
||||
R->>H : "转发到UpdateTexture处理器"
|
||||
H->>H : "参数绑定/校验"
|
||||
H->>S : "service.UpdateTexture(id, user_id, name, description, is_public?)"
|
||||
S->>REPO : "FindTextureByID(id)"
|
||||
REPO-->>S : "返回Texture或nil"
|
||||
S->>S : "权限校验:UploaderID==user_id"
|
||||
S->>S : "构建字段更新map仅非空/非零字段"
|
||||
S->>REPO : "UpdateTextureFields(id, updates)"
|
||||
REPO->>DB : "执行UPDATE"
|
||||
DB-->>REPO : "OK"
|
||||
S->>REPO : "FindTextureByID(id)"
|
||||
REPO-->>S : "返回更新后的Texture"
|
||||
S-->>H : "返回Texture"
|
||||
H-->>C : "200 成功响应"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L50-L60)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### UpdateTexture 接口:权限控制与字段级更新
|
||||
- 权限控制
|
||||
- 仅允许材质上传者修改;服务层通过比较 Texture 的 UploaderID 与请求用户ID进行校验
|
||||
- 字段级更新策略
|
||||
- 仅当请求中的 Name 非空、Description 非空、IsPublic 指针非空时,才将其加入更新集合
|
||||
- 若无字段需要更新,则跳过数据库写入
|
||||
- 最终重新查询并返回更新后的材质对象
|
||||
- 认证要求
|
||||
- 需携带 Bearer Token 的 Authorization 头
|
||||
- 常见错误码
|
||||
- 401 未授权(缺失/无效Token)
|
||||
- 403 权限不足(非上传者)
|
||||
- 400 请求参数错误(如ID非法)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入UpdateTexture"]) --> Parse["解析请求参数与用户ID"]
|
||||
Parse --> Load["加载材质记录"]
|
||||
Load --> Exists{"是否存在且未删除?"}
|
||||
Exists --> |否| Err404["返回404/不存在"]
|
||||
Exists --> |是| Perm["校验权限:UploaderID==user_id"]
|
||||
Perm --> |否| Err403["返回403/权限不足"]
|
||||
Perm --> |是| Build["构建字段更新map仅非空/非零字段"]
|
||||
Build --> HasAny{"是否有字段需要更新?"}
|
||||
HasAny --> |否| Reload["直接重新查询并返回"]
|
||||
HasAny --> |是| Save["执行字段更新"]
|
||||
Save --> Reload["重新查询并返回"]
|
||||
Reload --> End(["结束"])
|
||||
Err404 --> End
|
||||
Err403 --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
|
||||
|
||||
章节来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
|
||||
|
||||
### DeleteTexture 接口:软删除与权限验证
|
||||
- 软删除实现
|
||||
- 仓储层通过将 Status 更新为删除态(-1)实现软删除,保留数据以满足审计与历史追踪
|
||||
- 权限验证
|
||||
- 仅允许材质上传者删除;服务层在删除前进行权限校验
|
||||
- 认证要求
|
||||
- 需携带 Bearer Token 的 Authorization 头
|
||||
- 常见错误码
|
||||
- 401 未授权
|
||||
- 403 权限不足
|
||||
- 400 请求参数错误(如ID非法)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "控制器(texture_handler.go)"
|
||||
participant S as "服务层(texture_service.go)"
|
||||
participant REPO as "仓储层(texture_repository.go)"
|
||||
participant DB as "数据库"
|
||||
C->>H : "DELETE /api/v1/texture/ : id"
|
||||
H->>S : "service.DeleteTexture(id, user_id)"
|
||||
S->>REPO : "FindTextureByID(id)"
|
||||
REPO-->>S : "返回Texture或nil"
|
||||
S->>S : "权限校验:UploaderID==user_id"
|
||||
S->>REPO : "DeleteTexture(id) -> 更新status=-1"
|
||||
REPO->>DB : "执行UPDATE"
|
||||
DB-->>REPO : "OK"
|
||||
S-->>H : "返回nil成功"
|
||||
H-->>C : "200 成功响应"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
|
||||
章节来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
|
||||
### ToggleFavorite 接口:收藏切换与计数同步
|
||||
- 功能流程
|
||||
- 检查材质是否存在且未删除
|
||||
- 查询当前用户是否已收藏
|
||||
- 已收藏:取消收藏并递减 FavoriteCount
|
||||
- 未收藏:添加收藏并递增 FavoriteCount
|
||||
- 返回新的收藏状态
|
||||
- 认证要求
|
||||
- 需携带 Bearer Token 的 Authorization 头
|
||||
- 常见错误码
|
||||
- 401 未授权
|
||||
- 400 请求参数错误或业务异常
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入ToggleFavorite"]) --> Load["加载材质记录"]
|
||||
Load --> Exists{"是否存在且未删除?"}
|
||||
Exists --> |否| Err["返回错误"]
|
||||
Exists --> |是| Check["查询是否已收藏"]
|
||||
Check --> Favorited{"已收藏?"}
|
||||
Favorited --> |是| Unfav["删除收藏记录"] --> Dec["递减FavoriteCount"] --> RetFalse["返回false"]
|
||||
Favorited --> |否| Fav["新增收藏记录"] --> Inc["递增FavoriteCount"] --> RetTrue["返回true"]
|
||||
RetFalse --> End(["结束"])
|
||||
RetTrue --> End
|
||||
Err --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
|
||||
|
||||
章节来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
|
||||
|
||||
### 查询接口:GetTexture、SearchTextures、GetUserTextures
|
||||
- GetTexture
|
||||
- 公开接口,无需认证
|
||||
- 根据ID查询材质,若不存在或已被软删除则返回404
|
||||
- SearchTextures
|
||||
- 公开接口,无需认证
|
||||
- 支持关键词、类型、公开筛选;分页参数默认值与范围校验
|
||||
- GetUserTextures
|
||||
- 需 JWT 鉴权
|
||||
- 仅返回当前用户上传且未删除的材质,支持分页
|
||||
- 分页机制
|
||||
- page 默认 1,pageSize 默认 20,最大 100
|
||||
- 数据过滤规则
|
||||
- SearchTextures:status=1;可按 is_public 过滤;按 name/description 模糊匹配
|
||||
- GetUserTextures:uploader_id=user_id 且 status!=-1
|
||||
- GetTexture:status!=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 Token,Token 是否有效
|
||||
- 403 权限不足
|
||||
- 确认请求用户是否为材质上传者;检查 Update/Delete/ToggleFavorite 的权限校验逻辑
|
||||
- 400 请求参数错误
|
||||
- 检查 ID 是否为合法整数;请求体字段是否符合绑定规则
|
||||
- 404 资源不存在
|
||||
- 确认材质是否存在且未被软删除;查询接口会过滤 status=-1 的记录
|
||||
- 分页异常
|
||||
- page/page_size 默认值与范围校验:page<1 设为 1,pageSize 超过 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
|
||||
- 字段:name、description、is_public(指针)
|
||||
- 说明:仅当字段非空/非零时参与更新
|
||||
- CreateTextureRequest
|
||||
- 字段:name、description、type、url、hash、size、is_public、is_slim
|
||||
- TextureInfo
|
||||
- 字段:id、uploader_id、name、description、type、url、hash、size、is_public、download_count、favorite_count、is_slim、status、created_at、updated_at
|
||||
- 统一响应模型
|
||||
- 成功:code=200,message=“操作成功”,data=业务数据
|
||||
- 错误:code=400/401/403/404/500,message=错误描述,error=详细错误信息(开发环境)
|
||||
|
||||
章节来源
|
||||
- [common.go](file://internal/types/common.go#L86-L152)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
@@ -1,306 +0,0 @@
|
||||
# 材质删除
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go)
|
||||
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
本专项文档聚焦“材质删除”API,围绕删除操作的权限验证机制展开,结合服务层与仓储层的实现,说明删除流程、成功响应、错误场景,以及删除对数据库记录的影响(软删除)与关联数据处理(收藏关系)。同时基于测试用例,确认删除权限校验逻辑与行为一致性。
|
||||
|
||||
## 项目结构
|
||||
与材质删除相关的代码分布在以下模块:
|
||||
- 路由注册:定义DELETE /api/v1/texture/:id接口
|
||||
- 处理器:解析参数、鉴权、调用服务层并返回响应
|
||||
- 服务层:执行业务规则(权限校验、存在性校验)、调用仓储层
|
||||
- 仓储层:执行数据库操作(软删除)
|
||||
- 数据模型:定义材质实体、收藏关系、下载日志等
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Routes["路由注册<br/>routes.go"] --> Handler["处理器<br/>texture_handler.go"]
|
||||
Handler --> Service["服务层<br/>texture_service.go"]
|
||||
Service --> Repo["仓储层<br/>texture_repository.go"]
|
||||
Repo --> Model["数据模型<br/>texture.go"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
|
||||
|
||||
## 核心组件
|
||||
- 路由与鉴权
|
||||
- DELETE /api/v1/texture/:id 由处理器绑定,使用鉴权中间件,要求携带有效令牌。
|
||||
- 处理器
|
||||
- 解析路径参数(材质ID),从上下文提取当前用户ID,调用服务层执行删除,并按错误码返回相应HTTP状态与响应体。
|
||||
- 服务层
|
||||
- 校验材质存在性;校验删除权限(仅上传者可删);调用仓储层执行软删除。
|
||||
- 仓储层
|
||||
- 将材质记录的status字段置为-1,实现软删除。
|
||||
- 数据模型
|
||||
- 材质实体包含UploaderID、Status等字段;收藏关系通过user_texture_favorites表维护。
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
|
||||
|
||||
## 架构总览
|
||||
下图展示从客户端到数据库的完整调用链路,包括鉴权、参数解析、权限校验、软删除与响应返回。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "路由(routes.go)"
|
||||
participant H as "处理器(texture_handler.go)"
|
||||
participant S as "服务层(texture_service.go)"
|
||||
participant P as "仓储层(texture_repository.go)"
|
||||
participant D as "数据库"
|
||||
C->>R : "DELETE /api/v1/texture/ : id"
|
||||
R->>H : "进入处理器"
|
||||
H->>H : "解析路径参数/校验JWT"
|
||||
H->>S : "service.DeleteTexture(db, textureID, uploaderID)"
|
||||
S->>P : "FindTextureByID(textureID)"
|
||||
P-->>S : "返回材质或nil"
|
||||
alt "材质不存在"
|
||||
S-->>H : "返回错误:材质不存在"
|
||||
H-->>C : "403 错误响应"
|
||||
else "材质存在"
|
||||
S->>S : "校验 uploaderID == 请求者ID"
|
||||
alt "非上传者"
|
||||
S-->>H : "返回错误:无权删除此材质"
|
||||
H-->>C : "403 错误响应"
|
||||
else "上传者本人"
|
||||
S->>P : "DeleteTexture(textureID)"
|
||||
P->>D : "UPDATE textures SET status=-1 WHERE id=..."
|
||||
D-->>P : "OK"
|
||||
P-->>S : "OK"
|
||||
S-->>H : "OK"
|
||||
H-->>C : "200 成功响应"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 删除API请求流程与权限验证
|
||||
- 路由与鉴权
|
||||
- DELETE /api/v1/texture/:id 由处理器绑定,使用鉴权中间件,要求携带有效令牌。
|
||||
- 参数解析与鉴权
|
||||
- 处理器从路径参数解析材质ID,从上下文提取当前用户ID;若缺失则返回未授权。
|
||||
- 权限验证
|
||||
- 服务层先查询材质是否存在且未被软删除;
|
||||
- 再校验请求者ID与材质的UploaderID一致,否则返回无权删除。
|
||||
- 执行删除
|
||||
- 通过仓储层将材质记录的status字段置为-1,完成软删除。
|
||||
- 响应
|
||||
- 成功返回200与通用成功响应体;错误返回403及错误信息。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> Parse["解析路径参数<br/>获取材质ID"]
|
||||
Parse --> Auth{"JWT鉴权通过?"}
|
||||
Auth --> |否| Resp401["返回401 未授权"]
|
||||
Auth --> |是| Load["查询材质记录"]
|
||||
Load --> Exists{"是否存在且未软删除?"}
|
||||
Exists --> |否| Resp403a["返回403 材质不存在/已删除"]
|
||||
Exists --> |是| Perm{"请求者ID==上传者ID?"}
|
||||
Perm --> |否| Resp403b["返回403 无权删除此材质"]
|
||||
Perm --> |是| SoftDel["软删除:设置status=-1"]
|
||||
SoftDel --> Resp200["返回200 成功"]
|
||||
Resp401 --> End(["结束"])
|
||||
Resp403a --> End
|
||||
Resp403b --> End
|
||||
Resp200 --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
|
||||
### 权限验证机制
|
||||
- 上传者身份判定
|
||||
- 服务层比较请求者的用户ID与材质记录中的UploaderID,仅当两者相等时才允许删除。
|
||||
- 存在性与状态校验
|
||||
- 查询材质时会忽略status=-1的记录(软删除),因此若返回nil,即视为“不存在”。
|
||||
- 测试覆盖
|
||||
- 单元测试包含“删除权限检查”的用例,验证相同ID允许删除、不同ID拒绝删除的行为。
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L315-L345)
|
||||
|
||||
### 删除API的成功响应与错误情况
|
||||
- 成功响应
|
||||
- HTTP 200,返回通用成功响应体(无额外数据)。
|
||||
- 常见错误
|
||||
- 401 未授权:缺少或无效的JWT令牌。
|
||||
- 400 参数错误:材质ID格式非法。
|
||||
- 403 无权删除:请求者非材质上传者。
|
||||
- 403 材质不存在:目标材质不存在或已被软删除。
|
||||
- 处理器侧错误分支
|
||||
- 参数解析失败返回400;
|
||||
- 服务层返回错误时统一转换为403(无权删除/不存在)。
|
||||
|
||||
章节来源
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
|
||||
### 数据库记录影响与关联数据处理
|
||||
- 删除策略
|
||||
- 采用软删除:将textures表的status字段置为-1,不物理移除记录。
|
||||
- 影响范围
|
||||
- 材质记录仍保留,便于审计与历史追踪;
|
||||
- 查询接口(如获取详情、搜索、我的材质)均会忽略status=-1的记录。
|
||||
- 关联数据
|
||||
- 收藏关系:删除操作不直接清理user_texture_favorites表中的收藏记录;
|
||||
- 若需清理收藏,应在业务层面另行设计或在仓储层扩展软删除时级联处理(当前实现未体现)。
|
||||
- 下载日志
|
||||
- 删除操作不涉及texture_download_logs表的清理。
|
||||
|
||||
章节来源
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
|
||||
|
||||
### 类图(代码级)
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Texture {
|
||||
+int64 id
|
||||
+int64 uploader_id
|
||||
+string name
|
||||
+string description
|
||||
+TextureType type
|
||||
+string url
|
||||
+string hash
|
||||
+int size
|
||||
+bool is_public
|
||||
+int download_count
|
||||
+int favorite_count
|
||||
+bool is_slim
|
||||
+int16 status
|
||||
+time created_at
|
||||
+time updated_at
|
||||
}
|
||||
class UserTextureFavorite {
|
||||
+int64 id
|
||||
+int64 user_id
|
||||
+int64 texture_id
|
||||
+time created_at
|
||||
}
|
||||
class TextureDownloadLog {
|
||||
+int64 id
|
||||
+int64 texture_id
|
||||
+*int64 user_id
|
||||
+string ip_address
|
||||
+string user_agent
|
||||
+time created_at
|
||||
}
|
||||
Texture --> UserTextureFavorite : "收藏关系"
|
||||
Texture --> TextureDownloadLog : "下载日志"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L42-L57)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L60-L71)
|
||||
|
||||
## 依赖分析
|
||||
- 组件耦合
|
||||
- 处理器依赖服务层;服务层依赖仓储层;仓储层依赖数据库访问工具。
|
||||
- 关键依赖链
|
||||
- 路由 -> 处理器 -> 服务层 -> 仓储层 -> 数据库
|
||||
- 可能的循环依赖
|
||||
- 当前文件间未发现循环导入;各层职责清晰,符合分层架构。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Routes["routes.go"] --> Handler["texture_handler.go"]
|
||||
Handler --> Service["texture_service.go"]
|
||||
Service --> Repo["texture_repository.go"]
|
||||
Repo --> DB["数据库"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
|
||||
|
||||
## 性能考虑
|
||||
- 查询与软删除
|
||||
- 删除前的查询与软删除均为单记录操作,复杂度低。
|
||||
- 索引与过滤
|
||||
- 材质查询通常按UploaderID、状态、公开性等条件过滤,建议确保相关列具备索引以提升查询效率。
|
||||
- 批量与事务
|
||||
- 当前删除为单条记录操作,无需事务;若未来扩展批量删除,需评估事务边界与回滚策略。
|
||||
|
||||
## 故障排查指南
|
||||
- 401 未授权
|
||||
- 检查请求头Authorization是否携带有效JWT;确认中间件已正确注入user_id。
|
||||
- 400 参数错误
|
||||
- 检查路径参数id是否为合法整数。
|
||||
- 403 无权删除
|
||||
- 确认当前用户ID与材质记录的UploaderID一致;核对服务层权限校验逻辑。
|
||||
- 403 材质不存在
|
||||
- 确认材质ID有效且未被软删除;检查仓储层查询是否正确忽略status=-1。
|
||||
- 日志定位
|
||||
- 处理器与服务层均记录错误日志,可据此快速定位问题。
|
||||
|
||||
章节来源
|
||||
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
|
||||
|
||||
## 结论
|
||||
- 权限验证严格:仅上传者可删除材质,服务层明确校验请求者ID与UploaderID一致性。
|
||||
- 删除策略为软删除:通过status字段标记删除,不破坏历史数据与关联完整性。
|
||||
- 错误处理清晰:处理器将服务层错误映射为403,配合日志便于排障。
|
||||
- 关联清理:当前实现未清理收藏关系,如需可在业务层补充或扩展仓储层软删除逻辑。
|
||||
@@ -1,243 +0,0 @@
|
||||
# 材质搜索
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [texture_service_test.go](file://internal/service/texture_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向“材质搜索API”的使用与实现,围绕关键词搜索、材质类型过滤、公开性筛选与分页功能进行深入解析。基于仓库中的 SearchTextures 查询流程,解释关键词匹配(名称与描述)、类型过滤、公开状态筛选的实现细节,并结合分页测试用例说明分页参数的处理规则(page小于1时设为1,pageSize超过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 设为1;pageSize<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→1;pageSize<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_size,Limit=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)
|
||||
@@ -1,411 +0,0 @@
|
||||
# 材质收藏
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql)
|
||||
- [texture_service_test.go](file://internal/service/texture_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件围绕“材质收藏”能力进行系统化文档化,重点覆盖以下方面:
|
||||
- 收藏/取消收藏的切换逻辑与幂等性设计
|
||||
- 用户收藏列表的查询机制与分页实现
|
||||
- API 的请求参数、响应格式与错误处理
|
||||
- 基于测试用例的逻辑验证与正确性保障
|
||||
- 数据模型与数据库结构支撑
|
||||
|
||||
## 项目结构
|
||||
与“材质收藏”直接相关的代码分布在如下层次:
|
||||
- 路由层:定义收藏相关接口路径
|
||||
- 处理器层:解析请求、调用服务层、封装响应
|
||||
- 服务层:业务逻辑(收藏切换、收藏列表查询)
|
||||
- 仓储层:数据库访问(收藏状态判断、收藏增删、收藏计数增减、收藏列表查询)
|
||||
- 模型层:数据结构(材质、收藏关系、下载日志)
|
||||
- 数据库脚本:表结构与索引定义
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Routes["路由层<br/>routes.go"] --> Handler["处理器层<br/>texture_handler.go"]
|
||||
Handler --> Service["服务层<br/>texture_service.go"]
|
||||
Service --> Repo["仓储层<br/>texture_repository.go"]
|
||||
Service --> Model["模型层<br/>texture.go"]
|
||||
Repo --> DB["数据库脚本<br/>carrotskin_postgres.sql"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L57)
|
||||
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L42-L61)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L57)
|
||||
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
|
||||
|
||||
## 核心组件
|
||||
- 收藏切换接口:POST /api/v1/texture/{id}/favorite
|
||||
- 收藏列表接口:GET /api/v1/texture/favorites
|
||||
- 数据模型:材质、用户-材质收藏关系、下载日志
|
||||
- 仓储方法:收藏状态判断、收藏增删、收藏计数增减、收藏列表查询
|
||||
|
||||
章节来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L57)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
|
||||
|
||||
## 架构总览
|
||||
收藏能力的端到端流程如下:
|
||||
- 客户端向收藏接口发起请求
|
||||
- 路由层匹配到处理器
|
||||
- 处理器解析参数、调用服务层
|
||||
- 服务层根据收藏状态决定新增或删除收藏,并同步更新收藏计数
|
||||
- 仓储层执行数据库操作
|
||||
- 处理器封装响应返回客户端
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Routes as "路由层"
|
||||
participant Handler as "处理器"
|
||||
participant Service as "服务层"
|
||||
participant Repo as "仓储层"
|
||||
participant DB as "数据库"
|
||||
Client->>Routes : "POST /api/v1/texture/{id}/favorite"
|
||||
Routes->>Handler : "分发到 ToggleFavorite"
|
||||
Handler->>Service : "ToggleTextureFavorite(userID, textureID)"
|
||||
Service->>Repo : "IsTextureFavorited(userID, textureID)"
|
||||
Repo-->>Service : "布尔结果"
|
||||
alt 已收藏
|
||||
Service->>Repo : "RemoveTextureFavorite(userID, textureID)"
|
||||
Service->>Repo : "DecrementTextureFavoriteCount(textureID)"
|
||||
Repo-->>Service : "OK"
|
||||
else 未收藏
|
||||
Service->>Repo : "AddTextureFavorite(userID, textureID)"
|
||||
Service->>Repo : "IncrementTextureFavoriteCount(textureID)"
|
||||
Repo-->>Service : "OK"
|
||||
end
|
||||
Service-->>Handler : "返回新的收藏状态"
|
||||
Handler-->>Client : "200 {is_favorited : bool}"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L57-L57)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 收藏切换 API(ToggleTextureFavorite)
|
||||
- 接口路径:POST /api/v1/texture/{id}/favorite
|
||||
- 请求参数
|
||||
- 路径参数:id(材质ID)
|
||||
- 认证:Bearer Token(JWT)
|
||||
- 处理流程
|
||||
- 校验材质存在性
|
||||
- 查询当前用户是否已收藏
|
||||
- 若已收藏:删除收藏记录并减少收藏计数
|
||||
- 若未收藏:插入收藏记录并增加收藏计数
|
||||
- 返回布尔值表示新的收藏状态
|
||||
- 幂等性设计
|
||||
- 同一用户对同一材质重复调用收藏/取消收藏,最终状态与最后一次操作一致
|
||||
- 通过唯一约束避免重复收藏记录(见数据库脚本)
|
||||
- 错误处理
|
||||
- 材质不存在:返回错误
|
||||
- 数据库异常:返回错误
|
||||
- 未认证:返回 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)
|
||||
|
||||
### 用户收藏列表 API(GetUserTextureFavorites)
|
||||
- 接口路径:GET /api/v1/texture/favorites
|
||||
- 请求参数
|
||||
- page:页码,默认 1;最小 1
|
||||
- page_size:每页数量,默认 20;最小 1,最大 100
|
||||
- 认证:Bearer Token(JWT)
|
||||
- 处理流程
|
||||
- 校验分页参数边界
|
||||
- 通过子查询获取当前用户收藏的材质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)
|
||||
@@ -1,398 +0,0 @@
|
||||
# 创建与列表
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [profile_handler_test.go](file://internal/handler/profile_handler_test.go)
|
||||
- [common_test.go](file://internal/types/common_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者与集成方,系统性梳理“创建与列表”相关接口的实现与使用规范,重点覆盖:
|
||||
- 通过 POST /api/v1/profile/ 创建新档案,请求体中必须包含 1-16 字符的角色名;可选皮肤ID与披风ID。
|
||||
- 系统在创建时自动将该档案设为用户活跃档案,并将该用户其他档案置为非活跃。
|
||||
- 通过 GET /api/v1/profile/ 获取当前用户所有档案列表,响应包含档案UUID、名称、活跃状态、关联材质等信息。
|
||||
- 结合 profile_service.go 中的 CheckProfileLimit 逻辑说明用户档案数量上限(默认5个)的控制机制。
|
||||
|
||||
## 项目结构
|
||||
围绕档案模块的路由、处理器、服务层、仓储层与模型如下所示:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由层"
|
||||
R["routes.go<br/>注册 /api/v1/profile/* 路由"]
|
||||
end
|
||||
subgraph "处理器层"
|
||||
H["profile_handler.go<br/>CreateProfile / GetProfiles / SetActiveProfile 等"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["profile_service.go<br/>CreateProfile / GetUserProfiles / CheckProfileLimit 等"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
RP["profile_repository.go<br/>CreateProfile / FindProfilesByUserID / SetActiveProfile 等"]
|
||||
end
|
||||
subgraph "模型与类型"
|
||||
M["profile.go<br/>Profile / ProfileResponse / ProfileTexturesData 等"]
|
||||
T["texture.go<br/>Texture 类型"]
|
||||
C["common.go<br/>CreateProfileRequest / ProfileInfo / UpdateProfileRequest 等"]
|
||||
RESP["response.go<br/>统一响应结构"]
|
||||
end
|
||||
R --> H
|
||||
H --> S
|
||||
S --> RP
|
||||
RP --> M
|
||||
S --> M
|
||||
H --> RESP
|
||||
H --> C
|
||||
M --> T
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L57)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L31)
|
||||
- [common.go](file://internal/types/common.go#L81-L166)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
|
||||
## 核心组件
|
||||
- 路由注册:在路由层为档案模块注册了认证中间件保护的 POST / GET / PUT / DELETE / POST activate 等路径。
|
||||
- 处理器:负责解析请求、鉴权、调用服务层并返回统一响应。
|
||||
- 服务层:封装业务规则,如创建档案时的用户存在性校验、角色名唯一性校验、活跃状态设置、档案数量上限检查等。
|
||||
- 仓储层:封装数据库访问,如创建档案、查询用户档案列表、批量设置活跃状态等。
|
||||
- 模型与类型:定义档案实体、响应结构、请求体结构以及材质类型。
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L57)
|
||||
- [common.go](file://internal/types/common.go#L81-L166)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
## 架构总览
|
||||
下图展示从客户端到数据库的调用链路与关键步骤。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Router as "路由层(routes.go)"
|
||||
participant Handler as "处理器(profile_handler.go)"
|
||||
participant Service as "服务层(profile_service.go)"
|
||||
participant Repo as "仓储层(profile_repository.go)"
|
||||
participant DB as "数据库"
|
||||
Client->>Router : "POST /api/v1/profile"
|
||||
Router->>Handler : "CreateProfile"
|
||||
Handler->>Handler : "解析请求体(CreateProfileRequest)"
|
||||
Handler->>Handler : "读取用户ID(鉴权)"
|
||||
Handler->>Service : "CheckProfileLimit(userID, 5)"
|
||||
Service->>Repo : "CountProfilesByUserID(userID)"
|
||||
Repo->>DB : "统计数量"
|
||||
DB-->>Repo : "count"
|
||||
Repo-->>Service : "返回count"
|
||||
Service-->>Handler : "通过/错误"
|
||||
alt "未达上限"
|
||||
Handler->>Service : "CreateProfile(userID, name)"
|
||||
Service->>Repo : "FindUserByID(userID)"
|
||||
Repo->>DB : "查询用户"
|
||||
DB-->>Repo : "用户"
|
||||
Repo-->>Service : "返回用户"
|
||||
Service->>Repo : "FindProfileByName(name)"
|
||||
Repo->>DB : "查询角色名"
|
||||
DB-->>Repo : "结果"
|
||||
Service->>Repo : "CreateProfile(Profile)"
|
||||
Repo->>DB : "插入档案"
|
||||
Service->>Repo : "SetActiveProfile(uuid, userID)"
|
||||
Repo->>DB : "事务 : 将其他档案置为非活跃<br/>并将当前档案置为活跃"
|
||||
DB-->>Repo : "提交"
|
||||
Repo-->>Service : "返回Profile"
|
||||
Service-->>Handler : "返回Profile"
|
||||
Handler-->>Client : "200 成功(统一响应)"
|
||||
else "已达上限"
|
||||
Handler-->>Client : "400 参数错误(已达上限)"
|
||||
end
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L69)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### POST /api/v1/profile/ 创建档案
|
||||
- 功能概述:创建新的Minecraft角色档案,系统自动生成UUID并默认设为活跃状态,同时将该用户其他档案置为非活跃。
|
||||
- 请求体
|
||||
- 必填:name(字符串,1-16字符)
|
||||
- 可选:skin_id(整数,材质ID)、cape_id(整数,材质ID)
|
||||
- 响应
|
||||
- 成功:返回统一响应结构,data为 ProfileInfo 对象,包含 uuid、user_id、name、skin_id、cape_id、is_active、last_used_at、created_at、updated_at。
|
||||
- 失败:根据错误类型返回 400(参数错误/已达上限)、401(未授权)、500(服务器错误)。
|
||||
- 关键业务逻辑
|
||||
- 用户存在性与状态校验
|
||||
- 角色名唯一性校验
|
||||
- 档案数量上限检查(默认5个)
|
||||
- 创建成功后自动设置活跃状态,并将其他档案置为非活跃
|
||||
- 请求示例
|
||||
- 方法:POST
|
||||
- URL:/api/v1/profile
|
||||
- 请求头:Authorization: Bearer <token>
|
||||
- 请求体:
|
||||
- name: "PlayerName"
|
||||
- skin_id: 123(可选)
|
||||
- cape_id: 456(可选)
|
||||
- 响应示例
|
||||
- 成功:
|
||||
- code: 200
|
||||
- message: "操作成功"
|
||||
- data: {
|
||||
uuid: "550e8400-e29b-41d4-a716-446655440000"
|
||||
user_id: 1
|
||||
name: "PlayerName"
|
||||
skin_id: 123
|
||||
cape_id: 456
|
||||
is_active: true
|
||||
last_used_at: "2025-10-01T12:00:00Z"
|
||||
created_at: "2025-10-01T10:00:00Z"
|
||||
updated_at: "2025-10-01T10:00:00Z"
|
||||
}
|
||||
- 达到上限:
|
||||
- code: 400
|
||||
- message: "已达到档案数量上限(5个)"
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入 CreateProfile"]) --> Bind["绑定请求体(CreateProfileRequest)"]
|
||||
Bind --> CheckAuth{"鉴权通过?"}
|
||||
CheckAuth --> |否| Resp401["返回 401 未授权"]
|
||||
CheckAuth --> |是| Limit["CheckProfileLimit(userID, 5)"]
|
||||
Limit --> Over{"超过上限?"}
|
||||
Over --> |是| Resp400["返回 400 已达上限"]
|
||||
Over --> |否| Create["CreateProfile(userID, name)"]
|
||||
Create --> Exists{"用户存在且状态正常?"}
|
||||
Exists --> |否| Resp500["返回 500 用户异常"]
|
||||
Exists --> |是| Unique{"角色名唯一?"}
|
||||
Unique --> |否| Resp400["返回 400 角色名冲突"]
|
||||
Unique --> |是| Insert["插入档案记录"]
|
||||
Insert --> SetActive["SetActiveProfile(uuid, userID)<br/>将其他档案置为非活跃,当前置为活跃"]
|
||||
SetActive --> Done(["返回 200 成功"])
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L69)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L69)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
|
||||
- [common.go](file://internal/types/common.go#L81-L90)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
### GET /api/v1/profile/ 获取档案列表
|
||||
- 功能概述:返回当前用户的所有档案列表,包含每个档案的UUID、名称、活跃状态、关联材质等。
|
||||
- 请求
|
||||
- 方法:GET
|
||||
- URL:/api/v1/profile
|
||||
- 请求头:Authorization: Bearer <token>
|
||||
- 响应
|
||||
- 成功:返回统一响应结构,data为 ProfileInfo 数组。
|
||||
- 失败:401(未授权)、500(服务器错误)。
|
||||
- 关键逻辑
|
||||
- 服务层查询用户所有档案并预加载关联材质(Skin/Cape)
|
||||
- 处理器将模型转换为 ProfileInfo 并返回
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Router as "路由层(routes.go)"
|
||||
participant Handler as "处理器(profile_handler.go)"
|
||||
participant Service as "服务层(profile_service.go)"
|
||||
participant Repo as "仓储层(profile_repository.go)"
|
||||
participant DB as "数据库"
|
||||
Client->>Router : "GET /api/v1/profile"
|
||||
Router->>Handler : "GetProfiles"
|
||||
Handler->>Handler : "读取用户ID(鉴权)"
|
||||
Handler->>Service : "GetUserProfiles(userID)"
|
||||
Service->>Repo : "FindProfilesByUserID(userID)"
|
||||
Repo->>DB : "查询档案列表并预加载 Skin/Cape"
|
||||
DB-->>Repo : "返回 profiles"
|
||||
Repo-->>Service : "返回 profiles"
|
||||
Service-->>Handler : "返回 profiles"
|
||||
Handler->>Handler : "转换为 ProfileInfo 列表"
|
||||
Handler-->>Client : "200 成功(统一响应)"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L83-L90)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L44-L57)
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L83-L90)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L44-L57)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L31)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
### 档案数量上限与活跃状态控制
|
||||
- 档案数量上限
|
||||
- 默认上限为5个,来源于系统配置与处理器中的硬编码值。
|
||||
- 服务层提供 CheckProfileLimit(userID, maxProfiles) 进行检查。
|
||||
- 活跃状态控制
|
||||
- 创建新档案时,服务层会将该档案设为活跃,并通过仓储层的事务将该用户其他档案置为非活跃。
|
||||
- 提供 SetActiveProfile 接口用于手动切换活跃档案。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["创建/切换活跃"] --> B["CheckProfileLimit(userID, 5)"]
|
||||
B --> C{"未超限?"}
|
||||
C --> |是| D["CreateProfile 或 SetActiveProfile"]
|
||||
D --> E["SetActiveProfile(uuid, userID) 事务"]
|
||||
E --> F["将其他档案 is_active=false"]
|
||||
E --> G["将当前档案 is_active=true"]
|
||||
C --> |否| H["返回 400 达到上限"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L52-L63)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L190-L202)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L52-L63)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L190-L202)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
|
||||
|
||||
## 依赖分析
|
||||
- 路由层依赖处理器层,处理器层依赖服务层,服务层依赖仓储层,仓储层依赖数据库与模型。
|
||||
- 处理器层与服务层均依赖统一响应结构与请求/响应类型定义。
|
||||
- 档案模型与材质模型存在外键关联,查询时进行预加载以减少N+1问题。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Routes["routes.go"] --> Handler["profile_handler.go"]
|
||||
Handler --> Service["profile_service.go"]
|
||||
Service --> Repo["profile_repository.go"]
|
||||
Repo --> Model["profile.go"]
|
||||
Model --> Texture["texture.go"]
|
||||
Handler --> Types["common.go"]
|
||||
Handler --> Resp["response.go"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L57)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L31)
|
||||
- [common.go](file://internal/types/common.go#L81-L166)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L15-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L202)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L118)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L57)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L31)
|
||||
- [common.go](file://internal/types/common.go#L81-L166)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
## 性能考虑
|
||||
- 预加载关联材质:仓储层在查询用户档案列表时预加载 Skin 与 Cape,避免多次查询。
|
||||
- 事务一致性:设置活跃状态采用数据库事务,确保原子性与一致性。
|
||||
- 唯一性约束:角色名在模型层定义唯一索引,查询时可快速判定冲突。
|
||||
- 响应结构:统一响应结构便于前端处理与日志记录。
|
||||
|
||||
[本节为通用建议,不涉及具体文件分析]
|
||||
|
||||
## 故障排查指南
|
||||
- 400 参数错误
|
||||
- 角色名为空或长度不在1-16范围内
|
||||
- 已达到档案数量上限(默认5个)
|
||||
- 401 未授权
|
||||
- 缺少或无效的认证令牌
|
||||
- 403 权限不足
|
||||
- 操作他人档案(如更新/删除/设置活跃)
|
||||
- 404 资源不存在
|
||||
- 档案UUID不存在
|
||||
- 500 服务器错误
|
||||
- 数据库查询失败、事务提交失败、用户状态异常等
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L280)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L282-L339)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L341-L399)
|
||||
- [profile_handler_test.go](file://internal/handler/profile_handler_test.go#L40-L73)
|
||||
- [common_test.go](file://internal/types/common_test.go#L350-L383)
|
||||
|
||||
## 结论
|
||||
- 创建档案接口严格遵循请求体校验与业务规则,确保角色名唯一与数量上限控制。
|
||||
- 活跃状态切换通过事务保障一致性,避免并发场景下的状态不一致。
|
||||
- 档案列表接口提供完整档案信息与关联材质,满足前端展示需求。
|
||||
- 建议在生产环境中将上限值从硬编码迁移到系统配置中心,以便动态调整。
|
||||
|
||||
[本节为总结性内容,不涉及具体文件分析]
|
||||
|
||||
## 附录
|
||||
|
||||
### API 定义与数据结构
|
||||
|
||||
- POST /api/v1/profile
|
||||
- 请求体:CreateProfileRequest
|
||||
- name: string (必填,1-16字符)
|
||||
- skin_id: int64 (可选)
|
||||
- cape_id: int64 (可选)
|
||||
- 响应体:统一响应结构,data 为 ProfileInfo
|
||||
- uuid: string
|
||||
- user_id: int64
|
||||
- name: string
|
||||
- skin_id: int64 (可选)
|
||||
- cape_id: int64 (可选)
|
||||
- is_active: bool
|
||||
- last_used_at: datetime (可选)
|
||||
- created_at: datetime
|
||||
- updated_at: datetime
|
||||
|
||||
- GET /api/v1/profile
|
||||
- 响应体:统一响应结构,data 为 ProfileInfo 数组
|
||||
|
||||
章节来源
|
||||
- [common.go](file://internal/types/common.go#L81-L90)
|
||||
- [common.go](file://internal/types/common.go#L154-L166)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L95-L151)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
@@ -1,354 +0,0 @@
|
||||
# 更新与删除
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [manager.go](file://pkg/database/manager.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
本章节面向需要对接“档案更新与删除”接口的开发者,重点说明以下内容:
|
||||
- PUT /api/v1/profile/:uuid 的更新流程:支持修改档案名称、更换关联的皮肤或披风材质;强调“只能修改自己名下的档案”的权限校验。
|
||||
- DELETE /api/v1/profile/:uuid 的删除流程:删除前进行权限检查与档案存在性验证。
|
||||
- 请求体格式与错误处理策略(如403无权操作、404资源不存在)。
|
||||
- 结合 service 层代码解释更新操作的事务性保证与数据一致性维护机制。
|
||||
|
||||
## 项目结构
|
||||
围绕档案更新与删除功能,涉及如下关键模块:
|
||||
- 路由层:在路由中注册了 /api/v1/profile/{uuid} 的 PUT 与 DELETE 接口,并统一使用鉴权中间件。
|
||||
- 中间件层:JWT 鉴权中间件负责解析 Authorization 头并校验令牌有效性,将用户标识注入上下文。
|
||||
- 处理器层:profile_handler 负责接收请求、绑定参数、调用 service 并输出响应。
|
||||
- 服务层:profile_service 执行业务逻辑,包含权限校验、唯一性检查、更新与删除等。
|
||||
- 仓储层:profile_repository 封装数据库访问,提供查询、更新、删除与事务性操作。
|
||||
- 模型与类型:profile 模型定义档案字段及关联关系;types 定义请求与响应结构。
|
||||
- 数据库与鉴权:GORM 管理数据库连接;JWT 用于用户身份验证。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> Routes["路由: /api/v1/profile/{uuid}"]
|
||||
Routes --> AuthMW["鉴权中间件: JWT"]
|
||||
AuthMW --> Handler["处理器: profile_handler"]
|
||||
Handler --> Service["服务: profile_service"]
|
||||
Service --> Repo["仓储: profile_repository"]
|
||||
Repo --> DB["数据库: GORM"]
|
||||
Handler --> Model["模型: Profile"]
|
||||
Handler --> Types["类型: UpdateProfileRequest"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L197-L339)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L92-L159)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L77)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L29)
|
||||
- [common.go](file://internal/types/common.go#L201-L206)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
|
||||
## 核心组件
|
||||
- 路由注册:在路由组 /api/v1/profile 下注册了 GET/:uuid、POST/、GET/、PUT/:uuid、DELETE/:uuid、POST/:uuid/activate 等接口,其中 PUT 与 DELETE 对应本节主题。
|
||||
- 鉴权中间件:要求 Authorization 头为 Bearer 令牌,校验通过后将 user_id 等信息写入上下文。
|
||||
- 处理器:UpdateProfile 与 DeleteProfile 分别调用 service 层执行业务逻辑,并根据 service 返回的错误映射为合适的 HTTP 状态码。
|
||||
- 服务层:UpdateProfile 与 DeleteProfile 在执行前均进行“档案存在性验证”和“权限校验”,并在必要时使用数据库事务保证一致性。
|
||||
- 仓储层:FindProfileByUUID、UpdateProfile、DeleteProfile 提供基础 CRUD;SetActiveProfile 使用事务确保“同一用户仅有一个活跃档案”。
|
||||
|
||||
章节来源
|
||||
- [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 已具备完善的权限校验与存在性验证机制。
|
||||
- 更新操作支持名称与材质字段的灵活更新,删除操作简洁可靠。
|
||||
- 服务层与仓储层清晰分离职责,当前更新与删除为单步操作;若未来扩展为多步更新,建议引入事务以保障一致性。
|
||||
- 建议在数据库层面完善唯一性约束,并在服务层增加关键步骤的日志埋点,以便于问题定位与性能优化。
|
||||
@@ -1,336 +0,0 @@
|
||||
# 档案API
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [postgres.go](file://pkg/database/postgres.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者与集成方,系统化梳理“档案API”的设计与使用方法,覆盖以下能力:
|
||||
- 档案的创建、查询、更新、删除与“活跃档案”设置
|
||||
- 公开获取档案详情与需要认证的档案管理端点
|
||||
- 档案与Minecraft用户UUID的关系、活跃档案的概念
|
||||
- 档案列表的获取方式
|
||||
- 完整的API使用示例,包括创建档案的请求体结构与获取档案详情的响应格式
|
||||
- 档案与材质之间的关系
|
||||
|
||||
## 项目结构
|
||||
档案API位于路由组 `/api/v1/profile` 下,采用“公开路由 + 认证路由”的分层设计:
|
||||
- 公开路由:通过档案UUID获取档案详情
|
||||
- 认证路由:需要携带JWT令牌,支持创建、查询列表、更新、删除、设置活跃档案
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由组 /api/v1/profile"
|
||||
A["GET /:uuid<br/>公开:获取档案详情"]
|
||||
subgraph "认证组"
|
||||
B["POST /<br/>创建档案"]
|
||||
C["GET /<br/>获取我的档案列表"]
|
||||
D["PUT /:uuid<br/>更新档案"]
|
||||
E["DELETE /:uuid<br/>删除档案"]
|
||||
F["POST /:uuid/activate<br/>设置活跃档案"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
|
||||
## 核心组件
|
||||
- 路由注册:集中于路由文件,定义公开与认证端点
|
||||
- 处理器:profile_handler 负责鉴权、参数解析、调用服务层并返回统一响应
|
||||
- 服务层:profile_service 实现业务规则(如创建档案时生成UUID与RSA私钥、设置活跃状态、权限校验等)
|
||||
- 仓储层:profile_repository 封装数据库操作(增删改查、统计、事务)
|
||||
- 类型与模型:types 中定义请求/响应结构;model 中定义档案与材质模型及响应结构
|
||||
- 数据库:通过GORM连接PostgreSQL,统一日志与连接池配置
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [common.go](file://internal/types/common.go#L81-L207)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
|
||||
## 架构总览
|
||||
档案API遵循经典的三层架构:HTTP路由 -> 处理器 -> 服务 -> 仓储 -> 数据库。认证中间件确保仅持有有效令牌的用户可访问受保护端点。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> R["Gin 路由<br/>/api/v1/profile"]
|
||||
R --> M["认证中间件"]
|
||||
M --> H["处理器<br/>profile_handler"]
|
||||
H --> S["服务层<br/>profile_service"]
|
||||
S --> Repo["仓储层<br/>profile_repository"]
|
||||
Repo --> DB["数据库<br/>PostgreSQL(GORM)"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 路由与端点
|
||||
- 公开端点
|
||||
- GET /api/v1/profile/:uuid:根据UUID获取档案详情
|
||||
- 认证端点
|
||||
- POST /api/v1/profile:创建档案
|
||||
- GET /api/v1/profile:获取当前用户的所有档案列表
|
||||
- PUT /api/v1/profile/:uuid:更新档案(可更新角色名、皮肤ID、披风ID)
|
||||
- DELETE /api/v1/profile/:uuid:删除档案
|
||||
- POST /api/v1/profile/:uuid/activate:设置活跃档案(同时将该用户其他档案设为非活跃)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
|
||||
### 处理器与鉴权
|
||||
- 认证中间件:所有认证端点均使用认证中间件,从上下文提取user_id
|
||||
- 参数绑定:使用Gin的ShouldBindJSON进行请求体校验
|
||||
- 统一响应:使用统一响应结构,错误码与消息标准化
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
### 服务层业务规则
|
||||
- 创建档案
|
||||
- 校验用户存在且状态正常
|
||||
- 校验角色名唯一
|
||||
- 生成UUID与RSA私钥(PEM格式)
|
||||
- 默认设置为活跃档案,并将该用户其他档案设为非活跃
|
||||
- 更新档案
|
||||
- 校验档案归属(仅档案所属用户可更新)
|
||||
- 校验新角色名唯一
|
||||
- 支持更新角色名、皮肤ID、披风ID
|
||||
- 删除档案
|
||||
- 校验档案归属
|
||||
- 设置活跃档案
|
||||
- 校验档案归属
|
||||
- 使用事务将该用户其他档案设为非活跃,再将目标档案设为活跃
|
||||
- 同步更新最后使用时间
|
||||
- 档案数量限制
|
||||
- 通过服务层检查当前用户档案数量是否超过上限
|
||||
|
||||
章节来源
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
|
||||
### 仓储层与数据库
|
||||
- 查询
|
||||
- FindProfileByUUID:按UUID查询并预加载皮肤与披风
|
||||
- FindProfilesByUserID:按用户ID查询并预加载皮肤与披风,按创建时间倒序
|
||||
- FindProfileByName:按角色名查询
|
||||
- 更新
|
||||
- UpdateProfile、UpdateProfileFields
|
||||
- 删除
|
||||
- DeleteProfile(按UUID删除)
|
||||
- 统计
|
||||
- CountProfilesByUserID
|
||||
- 活跃状态
|
||||
- SetActiveProfile:事务内将该用户其他档案设为非活跃,再将目标档案设为活跃
|
||||
- 最后使用时间
|
||||
- UpdateProfileLastUsedAt
|
||||
|
||||
章节来源
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
|
||||
### 模型与类型
|
||||
- 档案模型
|
||||
- 字段:UUID、用户ID、角色名、皮肤ID、披风ID、活跃状态、最后使用时间、创建/更新时间
|
||||
- 关联:User、Skin(Texture)、Cape(Texture)
|
||||
- 档案响应结构
|
||||
- ProfileResponse:包含UUID、角色名、textures(皮肤/披风)、活跃状态、最后使用时间、创建时间
|
||||
- ProfileTexturesData:包含SKIN与CAPE两个可选字段
|
||||
- ProfileTexture:包含URL与可选metadata(如模型类型)
|
||||
- 请求/响应类型
|
||||
- CreateProfileRequest:name(必填,1-16字符)
|
||||
- UpdateProfileRequest:name(可选)、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)
|
||||
@@ -1,288 +0,0 @@
|
||||
# 激活管理
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [manager.go](file://pkg/database/manager.go)
|
||||
- [postgres.go](file://pkg/database/postgres.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [main.go](file://cmd/server/main.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
本文件聚焦于“档案激活管理API”的设计与实现,围绕 POST /api/v1/profile/:uuid/activate 端点展开,解释“活跃档案”的概念及其在系统中的作用:代表用户当前在游戏中使用的角色外观。调用该接口后,系统通过数据库事务确保原子性:先将用户所有其他档案的 is_active 字段设为 false,再将指定 UUID 的档案设为 active 状态;同时,该操作会更新档案的 last_used_at 时间戳,用于追踪最近使用情况。本文还提供调用示例、成功/失败响应说明,并结合 repository 层的 SetActiveProfile 事务实现,说明数据一致性保障机制。
|
||||
|
||||
## 项目结构
|
||||
该模块位于典型的分层架构中:
|
||||
- 路由层:定义 API 路由与鉴权中间件绑定
|
||||
- 处理器层:接收请求、解析参数、调用服务层并输出响应
|
||||
- 服务层:编排业务流程,进行权限校验与调用仓库层
|
||||
- 仓库层:封装数据库操作,提供事务与字段更新能力
|
||||
- 模型层:定义数据结构与表映射
|
||||
- 数据库层:GORM 初始化、连接池与迁移
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由层"
|
||||
R["internal/handler/routes.go"]
|
||||
end
|
||||
subgraph "处理器层"
|
||||
H["internal/handler/profile_handler.go"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["internal/service/profile_service.go"]
|
||||
end
|
||||
subgraph "仓库层"
|
||||
RP["internal/repository/profile_repository.go"]
|
||||
end
|
||||
subgraph "模型层"
|
||||
M["internal/model/profile.go"]
|
||||
end
|
||||
subgraph "数据库层"
|
||||
DM["pkg/database/manager.go"]
|
||||
DP["pkg/database/postgres.go"]
|
||||
end
|
||||
subgraph "应用入口"
|
||||
MAIN["cmd/server/main.go"]
|
||||
end
|
||||
R --> H
|
||||
H --> S
|
||||
S --> RP
|
||||
RP --> DM
|
||||
DM --> DP
|
||||
S --> M
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
- [manager.go](file://pkg/database/manager.go#L13-L50)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [main.go](file://cmd/server/main.go#L41-L51)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
- [manager.go](file://pkg/database/manager.go#L13-L50)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [main.go](file://cmd/server/main.go#L41-L51)
|
||||
|
||||
## 核心组件
|
||||
- 路由注册:在路由组 /api/v1/profile 下注册 POST /:uuid/activate,绑定鉴权中间件
|
||||
- 处理器:从上下文提取用户ID与UUID,调用服务层设置活跃档案
|
||||
- 服务层:校验档案归属、调用仓库层执行事务设置活跃状态,并更新 last_used_at
|
||||
- 仓库层:使用 GORM 事务,先批量将用户其他档案置为非活跃,再将目标档案置为活跃;随后更新 last_used_at
|
||||
- 模型层:Profile 结构体包含 uuid、user_id、name、is_active、last_used_at 等字段
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
|
||||
## 架构总览
|
||||
下图展示了从客户端到数据库的完整调用链路,以及事务原子性保障。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "路由层<br/>routes.go"
|
||||
participant H as "处理器层<br/>profile_handler.go"
|
||||
participant S as "服务层<br/>profile_service.go"
|
||||
participant RP as "仓库层<br/>profile_repository.go"
|
||||
participant DB as "数据库(GORM)"
|
||||
C->>R : "POST /api/v1/profile/ : uuid/activate"
|
||||
R->>H : "绑定鉴权中间件后进入处理器"
|
||||
H->>H : "从上下文获取 user_id 并校验"
|
||||
H->>S : "调用 SetActiveProfile(db, uuid, user_id)"
|
||||
S->>RP : "FindProfileByUUID(uuid)"
|
||||
RP-->>S : "返回档案或错误"
|
||||
S->>S : "校验档案归属(user_id)"
|
||||
S->>RP : "SetActiveProfile(uuid, user_id) 事务"
|
||||
RP->>DB : "事务开始"
|
||||
DB-->>RP : "事务上下文"
|
||||
RP->>DB : "批量更新其他档案为非活跃"
|
||||
RP->>DB : "更新目标档案为活跃"
|
||||
RP-->>S : "提交事务"
|
||||
S->>RP : "UpdateProfileLastUsedAt(uuid)"
|
||||
RP->>DB : "更新 last_used_at"
|
||||
RP-->>S : "返回成功"
|
||||
S-->>H : "返回成功"
|
||||
H-->>C : "200 OK {message : 设置成功}"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 活跃档案的概念与作用
|
||||
- 概念:活跃档案是用户当前在游戏中使用的角色外观,系统通过 is_active 字段标识
|
||||
- 作用:确保同一用户在同一时刻仅有一个活跃档案,避免多角色外观冲突;同时通过 last_used_at 记录最近使用时间,便于审计与统计
|
||||
|
||||
章节来源
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
|
||||
### 接口定义与调用流程
|
||||
- 方法与路径:POST /api/v1/profile/:uuid/activate
|
||||
- 鉴权:需携带有效 JWT,处理器从上下文提取 user_id
|
||||
- 参数:
|
||||
- 路径参数 uuid:目标档案的唯一标识
|
||||
- 成功响应:200 OK,返回统一成功响应结构
|
||||
- 失败响应:
|
||||
- 401 未授权:缺少或无效的 JWT
|
||||
- 403 禁止访问:档案不属于当前用户
|
||||
- 404 资源不存在:档案不存在
|
||||
- 500 服务器错误:其他数据库或业务异常
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [response.go](file://internal/model/response.go#L20-L38)
|
||||
|
||||
### 处理器层实现要点
|
||||
- 从上下文获取 user_id,若缺失返回 401
|
||||
- 调用服务层 SetActiveProfile,根据错误类型映射为 404 或 403 或 500
|
||||
- 成功返回 200 OK
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [response.go](file://internal/model/response.go#L20-L38)
|
||||
|
||||
### 服务层业务逻辑
|
||||
- 校验档案是否存在与归属关系
|
||||
- 调用仓库层 SetActiveProfile 执行事务
|
||||
- 事务完成后调用 UpdateProfileLastUsedAt 更新 last_used_at
|
||||
|
||||
章节来源
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
|
||||
### 仓库层事务实现与数据一致性
|
||||
- 使用 GORM 事务,确保以下两个更新在同一事务中:
|
||||
1) 将用户所有档案的 is_active 设为 false
|
||||
2) 将指定 uuid 的档案 is_active 设为 true
|
||||
- 提交事务后,调用 UpdateProfileLastUsedAt 将 last_used_at 更新为当前时间
|
||||
- 该实现保证了“同一时刻仅有一个活跃档案”的强一致性
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入 SetActiveProfile 事务"]) --> BatchDeactivate["批量更新其他档案为非活跃"]
|
||||
BatchDeactivate --> TargetActivate["更新目标档案为活跃"]
|
||||
TargetActivate --> Commit{"事务提交成功?"}
|
||||
Commit --> |是| UpdateTimestamp["更新 last_used_at"]
|
||||
Commit --> |否| Rollback["回滚事务"]
|
||||
UpdateTimestamp --> End(["返回成功"])
|
||||
Rollback --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
|
||||
章节来源
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
|
||||
### 数据模型与字段说明
|
||||
- Profile 模型包含:
|
||||
- uuid:档案唯一标识
|
||||
- user_id:所属用户ID
|
||||
- name:角色名
|
||||
- is_active:是否为活跃档案
|
||||
- last_used_at:最后使用时间
|
||||
- created_at/updated_at:创建与更新时间
|
||||
|
||||
章节来源
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
|
||||
### 数据库连接与迁移
|
||||
- 应用启动时初始化数据库连接与 GORM 实例,执行 AutoMigrate 自动迁移
|
||||
- 连接池参数可配置,PostgreSQL 驱动通过 DSN 构建
|
||||
- Profile 表在迁移中创建,包含上述字段及索引
|
||||
|
||||
章节来源
|
||||
- [main.go](file://cmd/server/main.go#L41-L51)
|
||||
- [manager.go](file://pkg/database/manager.go#L13-L50)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [config.go](file://pkg/config/config.go#L34-L47)
|
||||
|
||||
## 依赖关系分析
|
||||
- 路由层依赖处理器层
|
||||
- 处理器层依赖服务层
|
||||
- 服务层依赖仓库层与模型层
|
||||
- 仓库层依赖数据库管理器与 GORM
|
||||
- 应用入口负责初始化数据库并执行迁移
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Routes["routes.go"] --> Handler["profile_handler.go"]
|
||||
Handler --> Service["profile_service.go"]
|
||||
Service --> Repo["profile_repository.go"]
|
||||
Repo --> DBMgr["database/manager.go"]
|
||||
DBMgr --> PG["database/postgres.go"]
|
||||
Main["cmd/server/main.go"] --> DBMgr
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
- [manager.go](file://pkg/database/manager.go#L13-L50)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [main.go](file://cmd/server/main.go#L41-L51)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
- [manager.go](file://pkg/database/manager.go#L13-L50)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [main.go](file://cmd/server/main.go#L41-L51)
|
||||
|
||||
## 性能考量
|
||||
- 事务内两次 UPDATE 操作,均基于 user_id 与 uuid 的过滤条件,建议在 user_id 上建立索引以提升批量更新效率
|
||||
- last_used_at 更新为单行更新,影响范围小
|
||||
- 数据库连接池参数可通过配置调整,以平衡并发与资源占用
|
||||
|
||||
[本节为通用性能建议,不直接分析具体文件]
|
||||
|
||||
## 故障排查指南
|
||||
- 401 未授权:确认请求头携带有效 JWT,且令牌未过期
|
||||
- 403 禁止访问:目标档案不属于当前用户,检查 uuid 对应的 user_id
|
||||
- 404 资源不存在:uuid 无效或档案已被删除
|
||||
- 500 服务器错误:数据库异常或服务层内部错误,查看日志定位具体原因
|
||||
- 事务一致性问题:若出现“多个活跃档案”,检查数据库索引与事务提交逻辑
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L354-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
|
||||
## 结论
|
||||
POST /api/v1/profile/:uuid/activate 端点通过严格的鉴权与事务控制,确保“同一用户仅有一个活跃档案”的数据一致性,并在成功后更新 last_used_at。该设计既满足业务需求,又通过数据库层的事务保障了原子性与可靠性。建议在生产环境中关注索引与连接池配置,以进一步优化性能与稳定性。
|
||||
@@ -1,299 +0,0 @@
|
||||
# 详情查询
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [postgres.go](file://pkg/database/postgres.go)
|
||||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
本文件面向“详情查询”API,聚焦 GET /api/v1/profile/:uuid 端点。该接口为公开路由,无需认证即可访问,用于根据Minecraft用户UUID获取档案的公开信息。响应体包含档案UUID、用户名、创建时间、最后使用时间、活跃状态,以及关联的皮肤和披风信息(若存在)。该接口在Minecraft客户端通过Yggdrasil协议连接服务器时,可被用于查询玩家档案信息,以支持皮肤/披风展示等场景。
|
||||
|
||||
## 项目结构
|
||||
围绕“详情查询”API的关键文件组织如下:
|
||||
- 路由注册:在路由层定义公开的 GET /api/v1/profile/:uuid,并将其挂载到 v1 分组下。
|
||||
- 处理器:实现 GetProfile 处理函数,负责参数解析、调用服务层、构造统一响应。
|
||||
- 服务层:封装业务逻辑,调用仓库层进行数据查询。
|
||||
- 仓库层:基于GORM执行数据库查询,预加载皮肤/披风关联。
|
||||
- 模型与类型:定义档案模型、响应结构、纹理类型等。
|
||||
- 数据库:PostgreSQL驱动与连接池配置。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由层"
|
||||
R["routes.go<br/>注册 /api/v1/profile/:uuid"]
|
||||
end
|
||||
subgraph "处理器层"
|
||||
H["profile_handler.go<br/>GetProfile 处理函数"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["profile_service.go<br/>GetProfileByUUID"]
|
||||
end
|
||||
subgraph "仓库层"
|
||||
REPO["profile_repository.go<br/>FindProfileByUUID"]
|
||||
end
|
||||
subgraph "模型与类型"
|
||||
M["profile.go<br/>Profile/ProfileResponse"]
|
||||
T["texture.go<br/>Texture"]
|
||||
RESP["response.go<br/>统一响应结构"]
|
||||
TYPES["common.go<br/>ProfileInfo"]
|
||||
end
|
||||
subgraph "数据库"
|
||||
DB["postgres.go<br/>PostgreSQL 连接与配置"]
|
||||
end
|
||||
R --> H
|
||||
H --> S
|
||||
S --> REPO
|
||||
REPO --> DB
|
||||
H --> RESP
|
||||
S --> M
|
||||
REPO --> M
|
||||
M --> T
|
||||
H --> TYPES
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L35)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
- [common.go](file://internal/types/common.go#L154-L165)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L13-L74)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
|
||||
## 核心组件
|
||||
- 路由层:在 v1 分组下注册 GET /api/v1/profile/:uuid,明确该端点为公开路由,无需JWT认证。
|
||||
- 处理器层:GetProfile 从路径参数读取 uuid,调用服务层查询档案,构造统一响应。
|
||||
- 服务层:GetProfileByUUID 调用仓库层查询档案;若未找到返回“档案不存在”错误。
|
||||
- 仓库层:FindProfileByUUID 使用 uuid 条件查询,并预加载 Skin 和 Cape 关联。
|
||||
- 模型与类型:Profile 定义档案字段及关联;ProfileResponse 为对外响应结构;ProfileInfo 为处理器侧返回结构;Texture 定义材质模型及其索引字段。
|
||||
- 统一响应:response.go 提供统一的 Response/Error 结构,便于前后端约定。
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
|
||||
- [profile.go](file://internal/model/profile.go#L7-L24)
|
||||
- [texture.go](file://internal/model/texture.go#L16-L35)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
- [common.go](file://internal/types/common.go#L154-L165)
|
||||
|
||||
## 架构总览
|
||||
下面的序列图展示了客户端调用 GET /api/v1/profile/:uuid 的典型流程,以及与Yggdrasil协议的集成场景。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Router as "Gin 路由"
|
||||
participant Handler as "GetProfile 处理器"
|
||||
participant Service as "profile_service.GetProfileByUUID"
|
||||
participant Repo as "profile_repository.FindProfileByUUID"
|
||||
participant DB as "PostgreSQL"
|
||||
Client->>Router : "GET /api/v1/profile/ : uuid"
|
||||
Router->>Handler : "路由分发"
|
||||
Handler->>Service : "GetProfileByUUID(uuid)"
|
||||
Service->>Repo : "FindProfileByUUID(uuid)"
|
||||
Repo->>DB : "SELECT ... WHERE uuid=?<br/>并预加载 Skin/Cape"
|
||||
DB-->>Repo : "Profile 记录"
|
||||
Repo-->>Service : "Profile 对象"
|
||||
Service-->>Handler : "Profile 对象"
|
||||
Handler-->>Client : "200 + 统一响应体"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L63-L79)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L195)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L19-L31)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L13-L74)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 接口定义与行为
|
||||
- 路由:公开端点 GET /api/v1/profile/:uuid,无需认证。
|
||||
- 请求参数:路径参数 uuid(Minecraft档案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
|
||||
- name:Minecraft角色名
|
||||
- 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协议下的集成需求。通过合理的数据库索引、关联预加载与连接池配置,可在高并发场景下保持稳定与高效。建议结合缓存与监控体系,持续优化查询性能与用户体验。
|
||||
@@ -1,357 +0,0 @@
|
||||
# 个人资料管理
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [user_handler.go](file://internal/handler/user_handler.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [user.go](file://internal/model/user.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [password.go](file://pkg/auth/password.go)
|
||||
- [user_handler_test.go](file://internal/handler/user_handler_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者与测试人员,系统性梳理“个人资料管理”相关API,重点覆盖:
|
||||
- GET /api/v1/user/profile:获取当前登录用户的详细信息
|
||||
- PUT /api/v1/user/profile:更新头像与密码(邮箱修改请使用独立接口)
|
||||
|
||||
文档将解释 UserInfo 数据结构各字段及 JSON 序列化规则;详述密码更新的安全机制(旧密码校验与新密码加密);说明头像 URL 的更新流程(预签名上传 URL 生成与最终更新)。同时,结合 user_handler.go 中的 GetUserProfile 与 UpdateUserProfile 函数,说明 JWT 中间件如何注入用户上下文,服务层如何调用仓库层执行持久化操作,并提供完整请求/响应示例(含成功与常见错误场景)。
|
||||
|
||||
## 项目结构
|
||||
围绕个人资料管理的代码组织遵循“控制器-中间件-服务-仓库-模型”的分层设计,路由在 v1 组下统一挂载,用户相关接口均受 JWT 认证保护。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> Router["Gin 路由器"]
|
||||
Router --> GroupV1["/api/v1 用户组<br/>启用 AuthMiddleware()"]
|
||||
GroupV1 --> GetUserProfile["GET /api/v1/user/profile"]
|
||||
GroupV1 --> UpdateUserProfile["PUT /api/v1/user/profile"]
|
||||
GroupV1 --> AvatarUploadURL["POST /api/v1/user/avatar/upload-url"]
|
||||
GroupV1 --> UpdateAvatar["PUT /api/v1/user/avatar"]
|
||||
GetUserProfile --> Handler["user_handler.GetUserProfile"]
|
||||
UpdateUserProfile --> Handler
|
||||
AvatarUploadURL --> Handler
|
||||
UpdateAvatar --> Handler
|
||||
Handler --> Middleware["AuthMiddleware()<br/>注入 user_id/username/role"]
|
||||
Handler --> Service["user_service.*"]
|
||||
Service --> Repo["user_repository.*"]
|
||||
Service --> Model["model.User"]
|
||||
Handler --> Types["types.UserInfo / UpdateUserRequest"]
|
||||
Handler --> Resp["model.Response / model.ErrorResponse"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L41)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
|
||||
- [user_service.go](file://internal/service/user_service.go#L124-L164)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
|
||||
- [common.go](file://internal/types/common.go#L42-L47)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L41)
|
||||
|
||||
## 核心组件
|
||||
- 控制器层(Handler)
|
||||
- GetUserProfile:从上下文提取 user_id,查询用户并返回 UserInfo
|
||||
- UpdateUserProfile:接收 UpdateUserRequest,按需更新密码与头像,返回最新 UserInfo
|
||||
- 中间件层(AuthMiddleware)
|
||||
- 解析 Authorization: Bearer <token>,校验 JWT 并将用户信息写入上下文
|
||||
- 服务层(Service)
|
||||
- GetUserByID:封装仓库查询
|
||||
- UpdateUserInfo / UpdateUserAvatar:封装仓库更新
|
||||
- ChangeUserPassword:校验旧密码并加密新密码后更新
|
||||
- 仓库层(Repository)
|
||||
- FindUserByID / UpdateUser / UpdateUserFields:数据库读写
|
||||
- 类型与模型
|
||||
- types.UpdateUserRequest:请求体定义
|
||||
- types.UserInfo:响应体定义
|
||||
- model.User:数据库映射模型
|
||||
- model.Response / model.ErrorResponse:统一响应结构
|
||||
|
||||
章节来源
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [user_service.go](file://internal/service/user_service.go#L124-L164)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
|
||||
- [common.go](file://internal/types/common.go#L42-L47)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
## 架构总览
|
||||
以下序列图展示“获取/更新用户资料”的端到端流程,包含 JWT 中间件、控制器、服务与仓库的交互。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "Gin 路由"
|
||||
participant M as "AuthMiddleware"
|
||||
participant H as "user_handler"
|
||||
participant S as "user_service"
|
||||
participant RP as "user_repository"
|
||||
participant DB as "数据库"
|
||||
C->>R : "GET /api/v1/user/profile"
|
||||
R->>M : "鉴权中间件"
|
||||
M-->>R : "注入 user_id/username/role"
|
||||
R->>H : "GetUserProfile"
|
||||
H->>S : "GetUserByID(user_id)"
|
||||
S->>RP : "FindUserByID(user_id)"
|
||||
RP->>DB : "SELECT ..."
|
||||
DB-->>RP : "User"
|
||||
RP-->>S : "User"
|
||||
S-->>H : "User"
|
||||
H-->>C : "200 + UserInfo"
|
||||
C->>R : "PUT /api/v1/user/profile"
|
||||
R->>M : "鉴权中间件"
|
||||
M-->>R : "注入 user_id/username/role"
|
||||
R->>H : "UpdateUserProfile"
|
||||
H->>H : "解析请求体 UpdateUserRequest"
|
||||
alt "更新密码"
|
||||
H->>S : "ChangeUserPassword(user_id, old, new)"
|
||||
S->>RP : "FindUserByID(user_id)"
|
||||
RP->>DB : "SELECT ..."
|
||||
DB-->>RP : "User"
|
||||
RP-->>S : "User"
|
||||
S->>S : "CheckPassword(旧密码)"
|
||||
S->>S : "HashPassword(新密码)"
|
||||
S->>RP : "UpdateUserFields(user_id, {password})"
|
||||
RP->>DB : "UPDATE ..."
|
||||
DB-->>RP : "OK"
|
||||
end
|
||||
alt "更新头像"
|
||||
H->>S : "UpdateUserInfo(User)"
|
||||
S->>RP : "UpdateUser(User)"
|
||||
RP->>DB : "SAVE ..."
|
||||
DB-->>RP : "OK"
|
||||
end
|
||||
H->>S : "GetUserByID(user_id)"
|
||||
S->>RP : "FindUserByID(user_id)"
|
||||
RP->>DB : "SELECT ..."
|
||||
DB-->>RP : "User"
|
||||
RP-->>S : "User"
|
||||
S-->>H : "User"
|
||||
H-->>C : "200 + 最新 UserInfo"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L27-L41)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
|
||||
- [user_service.go](file://internal/service/user_service.go#L124-L164)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### GET /api/v1/user/profile:获取当前用户资料
|
||||
- 功能概述
|
||||
- 仅限已登录用户访问,通过 Authorization: Bearer <token> 进行鉴权
|
||||
- 中间件将 user_id 写入上下文,控制器据此查询用户并返回 UserInfo
|
||||
- 请求与响应
|
||||
- 请求:Authorization: Bearer <token>
|
||||
- 成功响应:200 + model.Response{ data: types.UserInfo }
|
||||
- 常见错误:401 未授权、404 用户不存在
|
||||
- 数据结构与序列化
|
||||
- types.UserInfo 字段与 JSON 映射规则详见“附录-数据结构”
|
||||
- 错误处理
|
||||
- 未携带或无效的 Authorization 头:返回 401
|
||||
- 查询不到用户:返回 404
|
||||
|
||||
章节来源
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L17-L68)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [routes.go](file://internal/handler/routes.go#L27-L41)
|
||||
- [common.go](file://internal/types/common.go#L113-L125)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
### PUT /api/v1/user/profile:更新头像与密码
|
||||
- 功能概述
|
||||
- 支持同时更新头像 URL 与密码;若仅更新头像,可不提供密码字段
|
||||
- 密码更新安全机制:必须同时提供旧密码与新密码,服务层校验旧密码并通过 bcrypt 加密新密码后更新
|
||||
- 头像更新流程:先生成预签名上传 URL,上传完成后调用更新头像接口,最终返回最新 UserInfo
|
||||
- 请求体 UpdateUserRequest
|
||||
- avatar:可选,头像 URL
|
||||
- old_password:可选,修改密码时必填
|
||||
- new_password:可选,修改密码时必填
|
||||
- 安全机制与流程
|
||||
- 旧密码校验:服务层通过 bcrypt 校验
|
||||
- 新密码加密:服务层使用 bcrypt 生成哈希
|
||||
- 头像更新:控制器直接更新用户记录;若仅更新头像,服务层保存用户对象
|
||||
- 错误处理
|
||||
- 未授权:401
|
||||
- 参数错误:400(如仅提供新密码或仅提供旧密码)
|
||||
- 用户不存在:404
|
||||
- 服务器错误:500
|
||||
|
||||
章节来源
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L70-L193)
|
||||
- [user_service.go](file://internal/service/user_service.go#L141-L164)
|
||||
- [password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [common.go](file://internal/types/common.go#L42-L47)
|
||||
- [response.go](file://internal/model/response.go#L27-L53)
|
||||
|
||||
### 头像上传与更新流程(补充说明)
|
||||
- 生成预签名上传 URL
|
||||
- POST /api/v1/user/avatar/upload-url
|
||||
- 输入:file_name
|
||||
- 输出:post_url、form_data、avatar_url、expires_in
|
||||
- 更新头像 URL
|
||||
- PUT /api/v1/user/avatar
|
||||
- 输入:avatar_url(查询参数)
|
||||
- 输出:最新 UserInfo
|
||||
- 注意事项
|
||||
- 该流程与“更新资料”接口不同,前者用于生成上传凭证,后者用于将最终 URL 写入数据库
|
||||
|
||||
章节来源
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L195-L326)
|
||||
- [routes.go](file://internal/handler/routes.go#L34-L39)
|
||||
- [common.go](file://internal/types/common.go#L68-L80)
|
||||
|
||||
### UserInfo 数据结构与 JSON 序列化规则
|
||||
- 字段说明
|
||||
- id:用户唯一标识
|
||||
- username:用户名
|
||||
- email:邮箱
|
||||
- avatar:头像 URL
|
||||
- points:积分
|
||||
- role:角色(如 user)
|
||||
- status:账户状态(1 正常,0 禁用,-1 删除)
|
||||
- last_login_at:最近登录时间(可空)
|
||||
- created_at / updated_at:创建与更新时间
|
||||
- JSON 映射
|
||||
- model.User 中的 Password 字段在 JSON 中不返回(避免泄露)
|
||||
- 其他字段按 gorm 标签映射到 JSON
|
||||
|
||||
章节来源
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
- [common.go](file://internal/types/common.go#L113-L125)
|
||||
|
||||
### 请求/响应示例(含错误场景)
|
||||
- 获取资料(成功)
|
||||
- 请求:Authorization: Bearer <token>
|
||||
- 响应:200 + { code: 200, message: "操作成功", data: { id, username, email, avatar, points, role, status, last_login_at, created_at, updated_at } }
|
||||
- 更新资料(仅更新头像)
|
||||
- 请求体:{ avatar: "https://example.com/new-avatar.png" }
|
||||
- 响应:200 + 最新 UserInfo
|
||||
- 更新资料(仅更新密码)
|
||||
- 请求体:{ old_password: "...", new_password: "..." }
|
||||
- 响应:200 + 最新 UserInfo
|
||||
- 更新资料(参数错误)
|
||||
- 请求体:{ new_password: "..." }(缺少 old_password)
|
||||
- 响应:400 + { code: 400, message: "请求参数错误", error: "修改密码需要提供原密码" }
|
||||
- 未授权
|
||||
- 请求:未携带或无效 Authorization
|
||||
- 响应:401 + { code: 401, message: "未授权,请先登录" }
|
||||
- 用户不存在
|
||||
- 响应:404 + { code: 404, message: "资源不存在" }
|
||||
- 服务器错误
|
||||
- 响应:500 + { code: 500, message: "服务器内部错误" }
|
||||
|
||||
章节来源
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
|
||||
- [response.go](file://internal/model/response.go#L27-L53)
|
||||
- [user_handler_test.go](file://internal/handler/user_handler_test.go#L52-L106)
|
||||
|
||||
## 依赖分析
|
||||
- 控制器依赖
|
||||
- user_handler 依赖 gin 上下文中的 user_id,来自 AuthMiddleware
|
||||
- 控制器调用 user_service 的 GetUserByID、UpdateUserInfo、ChangeUserPassword、UpdateUserAvatar
|
||||
- 服务层依赖
|
||||
- user_service 依赖 user_repository 的 FindUserByID、UpdateUser、UpdateUserFields
|
||||
- 使用 pkg/auth 的 HashPassword 与 CheckPassword
|
||||
- 中间件依赖
|
||||
- AuthMiddleware 依赖 pkg/auth/jwt 的 ValidateToken,将 claims 写入上下文
|
||||
- 模型与类型
|
||||
- model.User 作为 ORM 映射
|
||||
- types.UserInfo / UpdateUserRequest 作为 API 层数据契约
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Handler["user_handler"] --> Service["user_service"]
|
||||
Handler --> Middleware["AuthMiddleware"]
|
||||
Handler --> Types["types.*"]
|
||||
Handler --> Resp["model.Response / ErrorResponse"]
|
||||
Service --> Repo["user_repository"]
|
||||
Service --> Auth["pkg/auth/*"]
|
||||
Middleware --> JWT["pkg/auth/jwt"]
|
||||
Repo --> Model["model.User"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L17-L193)
|
||||
- [user_service.go](file://internal/service/user_service.go#L124-L164)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L17-L69)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [common.go](file://internal/types/common.go#L42-L47)
|
||||
- [response.go](file://internal/model/response.go#L1-L86)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
|
||||
## 性能考虑
|
||||
- 数据库查询
|
||||
- GetUserByID 使用精确主键查询,复杂度 O(1),建议保持索引完善
|
||||
- 密码处理
|
||||
- bcrypt 默认成本较高,建议在高并发场景关注 CPU 开销;可通过配置调整成本值
|
||||
- 缓存策略
|
||||
- 对频繁读取的用户资料可在应用层引入缓存(如 Redis),减少数据库压力
|
||||
- 日志与可观测性
|
||||
- 控制器与服务层已记录关键错误日志,建议配合链路追踪与指标监控
|
||||
|
||||
## 故障排查指南
|
||||
- 401 未授权
|
||||
- 检查 Authorization 头是否为 Bearer <token> 格式
|
||||
- 确认 token 未过期且签名正确
|
||||
- 400 参数错误
|
||||
- 密码更新时必须同时提供 old_password 与 new_password
|
||||
- 头像 URL 必须为合法 URL
|
||||
- 404 用户不存在
|
||||
- 确认 user_id 是否有效;检查用户状态是否被禁用或删除
|
||||
- 500 服务器错误
|
||||
- 检查数据库连接与事务执行情况;查看服务层日志定位具体异常
|
||||
|
||||
章节来源
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L27-L193)
|
||||
- [user_handler_test.go](file://internal/handler/user_handler_test.go#L108-L152)
|
||||
- [response.go](file://internal/model/response.go#L27-L53)
|
||||
|
||||
## 结论
|
||||
个人资料管理 API 采用清晰的分层架构:JWT 中间件负责鉴权并将用户上下文注入控制器,控制器协调服务层完成业务逻辑,服务层通过仓库层与数据库交互。密码更新具备严格的旧密码校验与新密码加密流程,头像更新支持预签名上传与最终 URL 写入。通过统一的响应结构与完善的错误处理,系统在安全性与易用性之间取得平衡。
|
||||
|
||||
## 附录
|
||||
|
||||
### 数据结构与序列化规则
|
||||
- types.UserInfo
|
||||
- 字段:id、username、email、avatar、points、role、status、last_login_at、created_at、updated_at
|
||||
- JSON 映射:与 model.User 字段一致,除 password 不返回
|
||||
- types.UpdateUserRequest
|
||||
- 字段:avatar、old_password、new_password
|
||||
- 校验:密码更新时 old_password 与 new_password 必须同时提供
|
||||
- model.User(数据库映射)
|
||||
- 字段:id、username、password、email、avatar、points、role、status、properties、last_login_at、created_at、updated_at
|
||||
- JSON:password 不返回;其他字段按标签映射
|
||||
- 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)
|
||||
@@ -1,337 +0,0 @@
|
||||
# 头像管理
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go)
|
||||
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [internal/types/common.go](file://internal/types/common.go)
|
||||
- [internal/model/user.go](file://internal/model/user.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者与运维人员,完整说明“头像管理”功能的实现与使用,包括:
|
||||
- 生成头像上传URL的API:POST /api/v1/user/avatar/upload-url
|
||||
- 更新头像URL的API:PUT /api/v1/user/avatar
|
||||
- 客户端直传对象存储(MinIO/RustFS)的完整流程
|
||||
- upload_service与storage包的协作机制
|
||||
- 请求参数、返回字段、错误处理与900秒(15分钟)有效期说明
|
||||
- 提供可直接参考的curl示例
|
||||
|
||||
## 项目结构
|
||||
围绕头像管理的关键文件组织如下:
|
||||
- 路由注册:在路由层注册两个端点,分别对应生成上传URL与更新头像URL
|
||||
- 处理器:用户处理器包含两个方法,分别处理上述两个端点
|
||||
- 服务层:upload_service负责生成预签名URL;user_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:生成头像上传URL(POST /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:更新头像URL(PUT /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)
|
||||
|
||||
### 组件C:upload_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)
|
||||
@@ -1,384 +0,0 @@
|
||||
# 用户API
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go)
|
||||
- [internal/middleware/auth.go](file://internal/middleware/auth.go)
|
||||
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [internal/types/common.go](file://internal/types/common.go)
|
||||
- [internal/model/response.go](file://internal/model/response.go)
|
||||
- [internal/model/user.go](file://internal/model/user.go)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能与安全考量](#性能与安全考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者与集成方,系统化梳理用户管理API,覆盖以下能力:
|
||||
- 获取与更新用户个人资料
|
||||
- 更换邮箱(基于验证码)
|
||||
- 头像上传(通过预签名URL)
|
||||
- JWT认证中间件的工作机制
|
||||
- 数据结构、请求/响应模式、错误处理与常见问题
|
||||
|
||||
## 项目结构
|
||||
用户相关接口位于统一的路由组“/api/v1/user”,该组下所有端点均受JWT认证保护。认证中间件负责从请求头中提取并校验Bearer Token,并将用户标识写入上下文供业务层使用。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> Routes["路由注册<br/>/api/v1/user/*"]
|
||||
Routes --> AuthMW["认证中间件<br/>AuthMiddleware()"]
|
||||
AuthMW --> Handlers["用户处理器<br/>user_handler.go"]
|
||||
Handlers --> Services["用户服务<br/>user_service.go"]
|
||||
Services --> Repos["用户仓储<br/>user_repository.go"]
|
||||
Services --> UploadSvc["上传服务<br/>upload_service.go"]
|
||||
UploadSvc --> Storage["对象存储客户端<br/>minio.go"]
|
||||
Services --> VeriSvc["验证码服务<br/>verification_service.go"]
|
||||
Services --> JWT["JWT服务<br/>jwt.go"]
|
||||
Services --> Types["类型定义<br/>types/common.go"]
|
||||
Handlers --> Models["响应模型<br/>response.go"]
|
||||
Handlers --> UserModel["用户模型<br/>model/user.go"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
|
||||
- [internal/middleware/auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [internal/handler/user_handler.go](file://internal/handler/user_handler.go#L17-L416)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L1-L161)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
|
||||
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L41)
|
||||
|
||||
## 核心组件
|
||||
- 路由与认证
|
||||
- 路由组“/api/v1/user”下的所有端点均使用认证中间件保护。
|
||||
- 中间件从Authorization头解析Bearer Token,调用JWT服务进行校验,并将用户ID、用户名、角色写入上下文。
|
||||
- 用户处理器
|
||||
- 提供获取/更新用户资料、生成头像上传URL、更新头像、更换邮箱等接口。
|
||||
- 服务层
|
||||
- 用户服务封装业务逻辑(如修改密码、更换邮箱、更新头像),并调用仓储层持久化。
|
||||
- 上传服务负责生成预签名URL(POST策略),并构造最终可访问的文件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
|
||||
- 请求体
|
||||
- 更新用户:UpdateUserRequest(avatar、old_password、new_password)
|
||||
- 生成头像上传URL:GenerateAvatarUploadURLRequest(file_name)
|
||||
- 更换邮箱:ChangeEmailRequest(new_email、verification_code)
|
||||
- 通用响应
|
||||
- 成功:code=200,message=“操作成功”,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
|
||||
- 请求体:UpdateUserRequest(avatar、old_password、new_password)
|
||||
- 成功响应:包含更新后的UserInfo
|
||||
- 生成头像上传URL
|
||||
- 方法:POST /api/v1/user/avatar/upload-url
|
||||
- 请求体:GenerateAvatarUploadURLRequest(file_name)
|
||||
- 成功响应:包含post_url、form_data、avatar_url、expires_in
|
||||
- 上传头像并确认
|
||||
- 方法:PUT /api/v1/user/avatar?avatar_url=...
|
||||
- 成功响应:包含UserInfo
|
||||
- 更换邮箱
|
||||
- 方法:POST /api/v1/user/change-email
|
||||
- 请求体:ChangeEmailRequest(new_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)
|
||||
@@ -1,336 +0,0 @@
|
||||
# 邮箱变更
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [user_handler.go](file://internal/handler/user_handler.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [verification_service.go](file://internal/service/verification_service.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
- [email.go](file://pkg/email/email.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能与可靠性](#性能与可靠性)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
本文档围绕“POST /api/v1/user/change-email”端点,系统化阐述邮箱变更的完整流程与技术实现细节。该接口要求用户提供新邮箱地址与验证码,后端通过验证服务核验Redis中存储的验证码,随后由用户服务层执行数据库更新,确保邮箱唯一性与安全性。文档还解释了请求体结构、验证码生成与存储机制、错误处理策略,并给出成功与失败场景的响应示例,强调该操作需要有效的JWT认证。
|
||||
|
||||
## 项目结构
|
||||
- 路由与鉴权
|
||||
- 路由在 v1 组下,用户相关接口均受JWT中间件保护。
|
||||
- 邮箱变更接口位于 /api/v1/user/change-email,采用POST方法。
|
||||
- 控制器层
|
||||
- 用户控制器负责接收请求、参数校验、调用服务层、组装响应。
|
||||
- 服务层
|
||||
- 验证服务:生成、发送、校验验证码;验证码以特定键规则存入Redis。
|
||||
- 用户服务:执行邮箱唯一性检查与数据库更新。
|
||||
- 数据访问层
|
||||
- 用户仓库封装GORM操作,提供按邮箱查询与字段更新能力。
|
||||
- 基础设施
|
||||
- Redis客户端:提供键值存取、过期控制、删除等操作。
|
||||
- 邮件服务:根据类型发送不同主题的验证码邮件。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> Routes["路由: /api/v1/user/change-email"]
|
||||
Routes --> AuthMW["JWT中间件"]
|
||||
AuthMW --> Handler["用户处理器: ChangeEmail"]
|
||||
Handler --> VerifySvc["验证服务: VerifyCode"]
|
||||
Handler --> UserSvc["用户服务: ChangeUserEmail"]
|
||||
VerifySvc --> Redis["Redis: 验证码存储"]
|
||||
UserSvc --> Repo["用户仓库: UpdateUserFields"]
|
||||
Repo --> DB["数据库: user 表"]
|
||||
VerifySvc --> Email["邮件服务: SendChangeEmail"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L120)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [email.go](file://pkg/email/email.go#L47-L55)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L120)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
|
||||
## 核心组件
|
||||
- 请求体结构
|
||||
- ChangeEmailRequest.new_email:新邮箱地址,必填且符合邮箱格式。
|
||||
- ChangeEmailRequest.verification_code:验证码,必填且长度为6。
|
||||
- 鉴权要求
|
||||
- 所有用户相关接口均需携带有效的Bearer JWT。
|
||||
- 验证码机制
|
||||
- 验证码类型常量包含“change_email”,用于区分不同用途。
|
||||
- 验证码长度为6,有效期10分钟,发送频率限制1分钟。
|
||||
- 验证通过后,Redis中的验证码键会被删除。
|
||||
- 邮箱唯一性校验
|
||||
- 在更新邮箱前,查询数据库确认新邮箱未被其他用户占用;若被占用则拒绝。
|
||||
- 错误处理
|
||||
- 参数错误、未授权、验证码错误、邮箱已被使用、服务器内部错误等均有明确响应码与消息。
|
||||
|
||||
章节来源
|
||||
- [common.go](file://internal/types/common.go#L62-L66)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||||
- [response.go](file://internal/model/response.go#L27-L53)
|
||||
|
||||
## 架构总览
|
||||
POST /api/v1/user/change-email 的端到端流程如下:
|
||||
1. 客户端携带JWT向 /api/v1/user/change-email 发起POST请求,请求体包含 new_email 与 verification_code。
|
||||
2. 路由匹配到用户组,JWT中间件校验通过后进入用户处理器。
|
||||
3. 处理器解析请求体并调用验证服务,验证Redis中对应类型的验证码是否匹配。
|
||||
4. 验证通过后,处理器调用用户服务更新数据库中的邮箱字段。
|
||||
5. 更新完成后,重新查询用户信息并返回成功响应。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "路由"
|
||||
participant M as "JWT中间件"
|
||||
participant H as "用户处理器"
|
||||
participant VS as "验证服务"
|
||||
participant RS as "Redis"
|
||||
participant US as "用户服务"
|
||||
participant UR as "用户仓库"
|
||||
participant DB as "数据库"
|
||||
C->>R : "POST /api/v1/user/change-email"
|
||||
R->>M : "鉴权"
|
||||
M-->>R : "通过"
|
||||
R->>H : "进入ChangeEmail"
|
||||
H->>VS : "VerifyCode(email, code, type=change_email)"
|
||||
VS->>RS : "Get(verification : code : change_email : new_email)"
|
||||
RS-->>VS : "验证码"
|
||||
VS-->>H : "验证通过"
|
||||
H->>US : "ChangeUserEmail(userID, new_email)"
|
||||
US->>UR : "FindUserByEmail(new_email)"
|
||||
UR-->>US : "查询结果"
|
||||
US->>UR : "UpdateUserFields(userID, {email : new_email})"
|
||||
UR->>DB : "更新"
|
||||
DB-->>UR : "成功"
|
||||
UR-->>US : "成功"
|
||||
US-->>H : "成功"
|
||||
H-->>C : "返回用户信息"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L27-L41)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
- [user_handler.go](file://internal/handler/user_handler.go#L328-L416)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [user_service.go](file://internal/service/user_service.go#L186-L201)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L65-L69)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 控制器:ChangeEmail 端点
|
||||
- 功能职责
|
||||
- 从上下文提取用户ID(JWT中间件注入)。
|
||||
- 解析请求体为 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与邮件服务可用,并合理配置验证码有效期与发送频率限制,以兼顾用户体验与安全。
|
||||
@@ -1,261 +0,0 @@
|
||||
# 系统API
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
|
||||
- [README.md](file://README.md)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件聚焦于系统相关的两个关键API:健康检查与系统配置获取。前者用于服务可用性监控,后者用于获取公开的全局配置项,帮助前端与运维侧快速了解站点状态与功能开关。文档将结合路由注册、处理器实现、模型与仓库层,给出端点定义、响应结构、调用流程与最佳实践,并说明Swagger文档的访问方式。
|
||||
|
||||
## 项目结构
|
||||
系统API位于内部处理层(handler),通过路由注册挂载至Gin引擎;系统配置模型与仓库层负责持久化与查询;Swagger文档通过中间件暴露静态页面与健康检查端点。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由与处理器"
|
||||
Routes["routes.go<br/>注册 /api/v1/system/config"]
|
||||
Swagger["swagger.go<br/>注册 /health 与 /swagger/*any"]
|
||||
Health["HealthCheck()<br/>返回 {status,message}"]
|
||||
SysCfg["GetSystemConfig()<br/>返回公开配置"]
|
||||
end
|
||||
subgraph "模型与仓库"
|
||||
ModelSC["SystemConfig 模型<br/>公开字段: site_name, registration_enabled, ..."]
|
||||
RepoSC["system_config_repository<br/>GetPublicSystemConfigs()"]
|
||||
end
|
||||
Routes --> SysCfg
|
||||
Swagger --> Health
|
||||
SysCfg --> RepoSC
|
||||
RepoSC --> ModelSC
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
|
||||
|
||||
## 核心组件
|
||||
- 健康检查端点
|
||||
- 路径:/health
|
||||
- 方法:GET
|
||||
- 作用:返回服务运行状态,便于Kubernetes、负载均衡器或监控系统进行存活探针与就绪探针
|
||||
- 响应:包含状态与消息字段的对象
|
||||
- 系统配置端点
|
||||
- 路径:/api/v1/system/config
|
||||
- 方法:GET
|
||||
- 作用:返回公开的系统配置集合,供前端渲染站点标题、描述、注册开关、用户限制等
|
||||
- 当前实现:返回硬编码的公开配置;后续可替换为从数据库读取
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
|
||||
## 架构总览
|
||||
系统API的调用链路如下:客户端请求 -> Gin路由 -> 处理器函数 -> 仓库层(当前为内存返回,未来可改为数据库查询)-> 返回响应。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "Gin路由"
|
||||
participant H as "处理器"
|
||||
participant M as "模型/仓库(当前为内存)"
|
||||
participant S as "Swagger/健康检查"
|
||||
C->>R : GET /health
|
||||
R->>S : HealthCheck()
|
||||
S-->>C : {status,message}
|
||||
C->>R : GET /api/v1/system/config
|
||||
R->>H : GetSystemConfig()
|
||||
H->>M : 当前返回硬编码公开配置
|
||||
M-->>H : 公开配置对象
|
||||
H-->>C : 成功响应
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 健康检查端点 /health
|
||||
- 注册位置:Swagger设置函数中注册了 /health 路由
|
||||
- 处理逻辑:返回固定的成功状态与消息
|
||||
- 响应结构:包含状态与消息字段的对象
|
||||
- 用途:作为Kubernetes探针、负载均衡器健康检查、CI流水线可用性检测
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["请求进入 /health"]) --> Check["调用 HealthCheck()"]
|
||||
Check --> BuildResp["构建响应对象<br/>包含 status 与 message"]
|
||||
BuildResp --> Return["返回 200 OK"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
|
||||
|
||||
### 系统配置端点 /api/v1/system/config
|
||||
- 注册位置:v1路由组下的 /system/config
|
||||
- 处理逻辑:当前实现返回硬编码的公开配置;注释提示后续应从数据库读取
|
||||
- 响应结构:当前返回包含站点名称、描述、注册开关、每用户材质与档案上限等字段的对象
|
||||
- 模型与仓库:
|
||||
- 模型定义了公开配置的结构(站点名称、描述、注册开关、维护模式、公告等)
|
||||
- 仓库提供了获取公开配置的方法(按 is_public 过滤)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "Gin路由"
|
||||
participant H as "GetSystemConfig()"
|
||||
participant Repo as "GetPublicSystemConfigs()"
|
||||
participant DB as "数据库"
|
||||
C->>R : GET /api/v1/system/config
|
||||
R->>H : 调用处理器
|
||||
alt 当前实现为内存返回
|
||||
H-->>C : 返回硬编码公开配置
|
||||
else 后续实现为数据库读取
|
||||
H->>Repo : 查询 is_public=true 的配置
|
||||
Repo->>DB : 执行查询
|
||||
DB-->>Repo : 返回配置列表
|
||||
Repo-->>H : 配置对象
|
||||
H-->>C : 返回公开配置
|
||||
end
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
|
||||
|
||||
### Swagger 文档访问
|
||||
- 路由:/swagger/*any
|
||||
- 作用:提供交互式API文档,便于开发者与测试人员查阅接口定义、参数与示例
|
||||
- 项目说明:README中明确给出了Swagger文档地址与健康检查地址
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
|
||||
- [README.md](file://README.md#L149-L153)
|
||||
|
||||
## 依赖分析
|
||||
- 路由注册依赖:
|
||||
- Swagger设置函数负责注册 /health 与 /swagger/*any
|
||||
- v1路由组负责注册 /api/v1/system/config
|
||||
- 处理器依赖:
|
||||
- GetSystemConfig 当前为内存返回;若改为数据库读取,则依赖仓库层的公开配置查询
|
||||
- 模型与仓库:
|
||||
- SystemConfig 模型定义公开配置字段
|
||||
- 仓库层提供按 is_public 过滤的查询方法
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Swagger["swagger.go"] --> Routes["routes.go"]
|
||||
Routes --> HandlerSys["GetSystemConfig()"]
|
||||
HandlerSys --> Repo["system_config_repository.go"]
|
||||
Repo --> Model["system_config.go"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L34)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L41)
|
||||
|
||||
## 性能考虑
|
||||
- 健康检查:纯内存返回,延迟极低,适合高频探针
|
||||
- 系统配置:
|
||||
- 当前为内存返回,避免数据库开销
|
||||
- 若切换为数据库查询,建议:
|
||||
- 对公开配置建立索引(如按 is_public 过滤)
|
||||
- 引入缓存层(如Redis)短期缓存公开配置,减少数据库压力
|
||||
- 控制响应大小,仅返回必要字段
|
||||
- Swagger文档:静态文件托管,对性能影响可忽略
|
||||
|
||||
## 故障排查指南
|
||||
- 健康检查失败
|
||||
- 检查服务进程是否正常运行
|
||||
- 确认 /health 路由已注册(Swagger设置函数)
|
||||
- 使用浏览器或curl访问 /health,观察返回状态与内容
|
||||
- 系统配置端点异常
|
||||
- 当前实现为内存返回,若返回异常,检查处理器逻辑
|
||||
- 若切换为数据库查询,检查数据库连接、表是否存在、字段是否正确
|
||||
- Swagger文档无法访问
|
||||
- 确认 /swagger/*any 路由已注册
|
||||
- 访问 /swagger/index.html 查看文档
|
||||
- 若文档为空,检查是否已生成Swagger文档
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L44)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [README.md](file://README.md#L149-L153)
|
||||
|
||||
## 结论
|
||||
- /health 提供轻量级服务可用性检查,适合作为探针与监控指标
|
||||
- /api/v1/system/config 当前返回硬编码公开配置,满足前端基础展示需求;后续可无缝替换为数据库读取,配合缓存与索引提升性能
|
||||
- Swagger文档提供便捷的接口浏览与调试入口,有助于开发与运维协作
|
||||
|
||||
## 附录
|
||||
|
||||
### API定义与响应示例
|
||||
|
||||
- 健康检查
|
||||
- 方法:GET
|
||||
- 路径:/health
|
||||
- 响应示例(结构示意):
|
||||
- status: 字符串,表示服务状态
|
||||
- message: 字符串,简要描述
|
||||
- 用途:存活/就绪探针、CI流水线可用性检测
|
||||
|
||||
- 系统配置
|
||||
- 方法:GET
|
||||
- 路径:/api/v1/system/config
|
||||
- 响应示例(结构示意):
|
||||
- site_name: 字符串,站点名称
|
||||
- site_description: 字符串,站点描述
|
||||
- registration_enabled: 布尔值,是否允许注册
|
||||
- max_textures_per_user: 整数,每用户最大材质数
|
||||
- max_profiles_per_user: 整数,每用户最大档案数
|
||||
- 用途:前端渲染站点标题、描述、注册开关、用户限制等
|
||||
|
||||
- Swagger文档
|
||||
- 访问路径:/swagger/index.html
|
||||
- 用途:查看与调试所有API接口
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L41-L62)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L112-L139)
|
||||
- [README.md](file://README.md#L149-L153)
|
||||
@@ -1,330 +0,0 @@
|
||||
# 发送验证码
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [verification_service.go](file://internal/service/verification_service.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [email.go](file://pkg/email/email.go)
|
||||
- [manager.go](file://pkg/email/manager.go)
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
- [manager.go](file://pkg/redis/manager.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [main.go](file://cmd/server/main.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向“发送验证码”API,围绕 /api/v1/auth/send-code 端点进行完整说明。内容包括:
|
||||
- 接口概述与HTTP规范
|
||||
- 请求体结构与字段约束(email、type)
|
||||
- 响应格式与错误码
|
||||
- 验证码生成、存储与过期策略(Redis)
|
||||
- 邮件服务集成(pkg/email)
|
||||
- 不同验证码类型(注册、重置密码、更换邮箱)的处理逻辑
|
||||
- 实际请求与响应示例
|
||||
- 安全注意事项(频率限制、邮箱格式)
|
||||
|
||||
## 项目结构
|
||||
/api/v1/auth/send-code 属于认证模块,位于 Gin 路由分组 /api/v1/auth 下,由处理器负责接收请求、绑定参数、调用服务层发送验证码,并通过 Redis 与邮件服务完成验证码的持久化与发送。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> Router["Gin 路由<br/>/api/v1/auth/send-code"]
|
||||
Router --> Handler["处理器<br/>SendVerificationCode"]
|
||||
Handler --> Service["服务层<br/>SendVerificationCode/VerifyCode"]
|
||||
Service --> Redis["Redis 客户端<br/>存储验证码/频率限制"]
|
||||
Service --> Email["邮件服务<br/>发送验证码邮件"]
|
||||
Email --> SMTP["SMTP 服务器"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [email.go](file://pkg/email/email.go#L29-L105)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
|
||||
## 核心组件
|
||||
- 路由与处理器
|
||||
- 路由注册:/api/v1/auth/send-code 绑定到 SendVerificationCode 处理器。
|
||||
- 处理器职责:参数绑定、调用服务层发送验证码、记录日志、返回统一响应。
|
||||
- 服务层
|
||||
- 生成6位数字验证码;按 type 与 email 组合键存储至 Redis,设置过期时间;设置发送频率限制;调用邮件服务发送对应类型的邮件。
|
||||
- Redis
|
||||
- 提供 Set/Get/Del/Exists 等基础操作,封装过期时间控制与错误处理。
|
||||
- 邮件服务
|
||||
- 基于 SMTP 的邮件发送,支持 465(隐式 TLS)与 587(显式 TLS)端口;根据 type 选择不同主题与正文模板。
|
||||
- 类型定义
|
||||
- SendVerificationCodeRequest 包含 email 与 type 字段,type 限定为 register/reset_password/change_email。
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [email.go](file://pkg/email/email.go#L29-L105)
|
||||
- [common.go](file://internal/types/common.go#L49-L54)
|
||||
|
||||
## 架构总览
|
||||
发送验证码的端到端流程如下:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "处理器<br/>SendVerificationCode"
|
||||
participant S as "服务层<br/>SendVerificationCode/VerifyCode"
|
||||
participant R as "Redis 客户端"
|
||||
participant E as "邮件服务<br/>SendVerificationCode"
|
||||
participant M as "SMTP 服务器"
|
||||
C->>H : POST /api/v1/auth/send-code<br/>{email, type}
|
||||
H->>H : 绑定请求体并校验
|
||||
H->>S : SendVerificationCode(ctx, redis, email, type)
|
||||
S->>R : Exists(rate_limit_key)
|
||||
alt 已存在
|
||||
S-->>H : 返回“发送过于频繁”
|
||||
H-->>C : 400 错误
|
||||
else 不存在
|
||||
S->>S : 生成6位数字验证码
|
||||
S->>R : Set(code_key, code, 10分钟)
|
||||
S->>R : Set(rate_limit_key, 1, 1分钟)
|
||||
S->>E : 根据type发送邮件
|
||||
E->>M : 发送邮件
|
||||
M-->>E : 成功/失败
|
||||
alt 邮件发送失败
|
||||
S->>R : Del(code_key)
|
||||
S-->>H : 返回“发送邮件失败”
|
||||
H-->>C : 400 错误
|
||||
else 成功
|
||||
S-->>H : 返回成功
|
||||
H-->>C : 200 成功
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [email.go](file://pkg/email/email.go#L29-L105)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### HTTP 端点定义
|
||||
- 方法:POST
|
||||
- 路径:/api/v1/auth/send-code
|
||||
- 功能:根据邮箱与类型发送验证码邮件,并在 Redis 中存储验证码及频率限制。
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
|
||||
### 请求体结构
|
||||
- 字段
|
||||
- email: 必填,需符合邮箱格式
|
||||
- type: 必填,枚举值为 register/reset_password/change_email
|
||||
- 参数绑定与校验
|
||||
- 使用 Gin 的绑定与验证机制,确保 email 符合邮箱格式,type 在允许范围内。
|
||||
|
||||
章节来源
|
||||
- [common.go](file://internal/types/common.go#L49-L54)
|
||||
|
||||
### 响应格式
|
||||
- 成功
|
||||
- 状态码:200
|
||||
- 结构:统一响应体,包含 message 字段提示“验证码已发送,请查收邮件”
|
||||
- 失败
|
||||
- 状态码:400
|
||||
- 结构:统一错误响应体,包含错误码与错误信息
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
|
||||
### 错误码与错误场景
|
||||
- 400 参数错误
|
||||
- 请求体绑定失败或参数校验失败
|
||||
- 400 发送过于频繁
|
||||
- Redis 中存在频率限制键(1分钟内)
|
||||
- 400 邮件发送失败
|
||||
- 邮件服务未启用或 SMTP 发送异常
|
||||
- 400 验证码已过期或不存在
|
||||
- 验证码校验时 Redis 中无对应键
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
|
||||
### 验证码生成与存储(Redis)
|
||||
- 生成规则
|
||||
- 6位数字验证码
|
||||
- 存储键
|
||||
- 验证码键:verification:code:{type}:{email}
|
||||
- 过期时间:10分钟
|
||||
- 频率限制
|
||||
- 频率限制键:verification:rate_limit:{type}:{email}
|
||||
- 过期时间:1分钟
|
||||
- Redis 操作
|
||||
- Set/SetEx:写入验证码与频率限制
|
||||
- Get:读取验证码用于校验
|
||||
- Del:验证成功后删除验证码键
|
||||
- Exists:检查是否处于发送冷却
|
||||
|
||||
章节来源
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
|
||||
### 邮件服务集成(pkg/email)
|
||||
- 初始化
|
||||
- 通过全局初始化函数完成一次性初始化,随后通过 MustGetService 获取实例
|
||||
- 发送逻辑
|
||||
- 根据 type 选择不同主题与正文模板
|
||||
- 支持 465(隐式 TLS)与 587(显式 TLS)两种端口模式
|
||||
- 当邮件服务未启用时,返回“邮件服务未启用”的错误
|
||||
- 主题与正文
|
||||
- 注册:邮箱验证
|
||||
- 重置密码:重置密码
|
||||
- 更换邮箱:更换邮箱验证
|
||||
- 默认:通用验证码
|
||||
|
||||
章节来源
|
||||
- [email.go](file://pkg/email/email.go#L29-L105)
|
||||
- [manager.go](file://pkg/email/manager.go#L1-L43)
|
||||
- [main.go](file://cmd/server/main.go#L71-L74)
|
||||
|
||||
### 不同验证码类型的处理逻辑
|
||||
- 注册(register)
|
||||
- 邮件主题:邮箱验证
|
||||
- 通常配合注册接口使用,注册时需提供验证码
|
||||
- 重置密码(reset_password)
|
||||
- 邮件主题:重置密码
|
||||
- 与 /api/v1/auth/reset-password 配合使用
|
||||
- 更换邮箱(change_email)
|
||||
- 邮件主题:更换邮箱验证
|
||||
- 与用户更换邮箱流程配合使用
|
||||
- 默认(其他)
|
||||
- 通用验证码主题
|
||||
|
||||
章节来源
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L106-L118)
|
||||
- [email.go](file://pkg/email/email.go#L107-L139)
|
||||
|
||||
### 安全考虑
|
||||
- 邮箱格式验证
|
||||
- 请求体中 email 字段使用邮箱格式校验
|
||||
- 发送频率限制
|
||||
- Redis 中按 type+email 维度设置1分钟冷却,避免刷屏
|
||||
- 验证码有效期
|
||||
- Redis 中验证码设置10分钟过期,过期即失效
|
||||
- 重复使用防护
|
||||
- 验证码校验成功后立即删除键,防止二次使用
|
||||
- 邮件服务开关
|
||||
- 若未启用邮件服务,发送验证码会直接失败,避免泄露敏感信息
|
||||
|
||||
章节来源
|
||||
- [common.go](file://internal/types/common.go#L49-L54)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [email.go](file://pkg/email/email.go#L29-L40)
|
||||
|
||||
## 依赖关系分析
|
||||
- 处理器依赖服务层
|
||||
- 服务层依赖 Redis 与邮件服务
|
||||
- 邮件服务依赖配置与日志
|
||||
- Redis 客户端依赖配置与日志
|
||||
- 全局初始化顺序:配置 -> 日志 -> 数据库 -> JWT -> Redis -> 对象存储 -> 邮件服务
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
H["处理器<br/>auth_handler.go"] --> S["服务层<br/>verification_service.go"]
|
||||
S --> R["Redis 客户端<br/>redis.go"]
|
||||
S --> E["邮件服务<br/>email.go"]
|
||||
E --> Cfg["配置<br/>config.go"]
|
||||
E --> Lg["日志"]
|
||||
R --> Cfg
|
||||
R --> Lg
|
||||
Main["服务启动<br/>main.go"] --> E
|
||||
Main --> R
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [email.go](file://pkg/email/email.go#L29-L105)
|
||||
- [config.go](file://pkg/config/config.go#L49-L107)
|
||||
- [main.go](file://cmd/server/main.go#L27-L74)
|
||||
|
||||
章节来源
|
||||
- [main.go](file://cmd/server/main.go#L27-L74)
|
||||
|
||||
## 性能考量
|
||||
- Redis 操作均为 O(1),Set/Get/Del/Exists 均为常数时间复杂度
|
||||
- 验证码长度固定为6位,生成与比较成本低
|
||||
- 邮件发送为外部依赖,受网络与SMTP服务器性能影响
|
||||
- 建议
|
||||
- 合理设置 Redis 连接池大小(PoolSize)
|
||||
- 控制邮件发送并发,避免瞬时高峰导致SMTP限流
|
||||
- 对高频请求开启更严格的频率限制(如增加冷却时间)
|
||||
|
||||
[本节为通用性能建议,不直接分析具体文件]
|
||||
|
||||
## 故障排查指南
|
||||
- 400 参数错误
|
||||
- 检查请求体是否包含 email 与 type,且 type 是否在允许范围内
|
||||
- 400 发送过于频繁
|
||||
- 等待1分钟冷却时间,或检查 Redis 中 rate_limit 键是否仍存在
|
||||
- 400 邮件发送失败
|
||||
- 确认邮件服务已启用(EMAIL_ENABLED=true)
|
||||
- 检查 SMTP 配置(主机、端口、用户名、密码、发件人名称)
|
||||
- 查看日志中 SMTP 发送错误信息
|
||||
- 400 验证码已过期或不存在
|
||||
- 检查 Redis 中 code 键是否存在与过期时间
|
||||
- 确认验证码是否已被验证成功后删除
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [email.go](file://pkg/email/email.go#L29-L105)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
|
||||
## 结论
|
||||
/api/v1/auth/send-code 端点通过 Gin 处理器接收请求,调用服务层完成验证码生成、Redis 存储与频率限制、邮件发送等流程。其设计遵循最小暴露面原则:严格参数校验、冷却时间、过期时间与删除机制共同保障安全性与可用性。结合 pkg/email 与 pkg/redis 的稳定实现,整体具备良好的扩展性与可维护性。
|
||||
|
||||
[本节为总结性内容,不直接分析具体文件]
|
||||
|
||||
## 附录
|
||||
|
||||
### 请求与响应示例
|
||||
- 请求示例
|
||||
- 方法:POST
|
||||
- 路径:/api/v1/auth/send-code
|
||||
- 请求体:
|
||||
- email: user@example.com
|
||||
- type: register 或 reset_password 或 change_email
|
||||
- 成功响应示例
|
||||
- 状态码:200
|
||||
- 响应体:包含 message 字段,提示“验证码已发送,请查收邮件”
|
||||
- 失败响应示例
|
||||
- 状态码:400
|
||||
- 响应体:包含错误码与错误信息,如“发送过于频繁,请稍后再试”或“发送邮件失败”
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [common.go](file://internal/types/common.go#L49-L54)
|
||||
@@ -1,352 +0,0 @@
|
||||
# 用户注册
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [verification_service.go](file://internal/service/verification_service.go)
|
||||
- [password.go](file://pkg/auth/password.go)
|
||||
- [user.go](file://internal/model/user.go)
|
||||
- [auth_handler_test.go](file://internal/handler/auth_handler_test.go)
|
||||
- [common_test.go](file://internal/types/common_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能与扩展性](#性能与扩展性)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向“用户注册”API,围绕 /api/v1/auth/register 端点进行完整说明。内容覆盖:
|
||||
- HTTP 方法与路由
|
||||
- 请求体字段与校验规则
|
||||
- 响应格式与错误码
|
||||
- 验证码机制与防暴力注册策略
|
||||
- 密码加密存储流程
|
||||
- 安全建议(密码强度、邮箱格式、防暴力注册)
|
||||
|
||||
## 项目结构
|
||||
该功能由 Handler 层接收请求、Service 层执行业务逻辑、Model 层承载数据模型,并通过 JWT 生成登录令牌;验证码由 Redis 缓存并在邮件服务中发送。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> Routes["路由: /api/v1/auth/register"]
|
||||
Routes --> Handler["处理器: Register"]
|
||||
Handler --> VerifyCode["验证码校验: VerifyCode"]
|
||||
Handler --> UserService["服务: RegisterUser"]
|
||||
UserService --> Password["密码加密: HashPassword"]
|
||||
UserService --> ModelUser["用户模型: User"]
|
||||
UserService --> JWT["JWT 服务: GenerateToken"]
|
||||
Handler --> Response["统一响应: Response/ErrorResponse"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L68)
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
- [response.go](file://internal/model/response.go#L20-L73)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
|
||||
## 核心组件
|
||||
- 路由与入口
|
||||
- 路由位于 v1 组下的 /auth/register,无需 JWT 即可访问。
|
||||
- 请求体与校验
|
||||
- 请求体字段:username、email、password、avatar、verification_code。
|
||||
- 校验规则:必填、长度/格式约束、验证码长度固定为6。
|
||||
- 业务处理
|
||||
- 验证码校验通过后,检查用户名/邮箱唯一性,加密密码,创建用户并生成 JWT。
|
||||
- 响应与错误
|
||||
- 成功返回包含 token 与用户信息的结构化响应;常见错误包括参数错误、验证码错误、用户名/邮箱已存在等。
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
|
||||
- [common.go](file://internal/types/common.go#L33-L40)
|
||||
- [response.go](file://internal/model/response.go#L20-L73)
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L68)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
|
||||
## 架构总览
|
||||
注册流程的端到端调用序列如下:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "路由"
|
||||
participant H as "处理器 : Register"
|
||||
participant VS as "验证码服务 : VerifyCode"
|
||||
participant US as "用户服务 : RegisterUser"
|
||||
participant PS as "密码服务 : HashPassword"
|
||||
participant M as "用户模型 : User"
|
||||
participant JS as "JWT服务 : GenerateToken"
|
||||
participant RESP as "响应"
|
||||
C->>R : POST /api/v1/auth/register
|
||||
R->>H : 转发请求
|
||||
H->>H : 解析并绑定请求体
|
||||
H->>VS : 校验邮箱验证码
|
||||
VS-->>H : 验证通过/失败
|
||||
alt 验证失败
|
||||
H-->>RESP : 返回400错误
|
||||
else 验证通过
|
||||
H->>US : 调用注册流程
|
||||
US->>US : 检查用户名/邮箱唯一性
|
||||
US->>PS : 加密密码
|
||||
PS-->>US : 密文
|
||||
US->>M : 创建用户记录
|
||||
US->>JS : 生成JWT
|
||||
JS-->>US : token
|
||||
US-->>H : 返回用户与token
|
||||
H-->>RESP : 返回200成功响应
|
||||
end
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L68)
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
- [response.go](file://internal/model/response.go#L20-L73)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 接口定义与请求体
|
||||
- 端点:POST /api/v1/auth/register
|
||||
- 请求体字段
|
||||
- username:必填,长度3~50
|
||||
- email:必填,邮箱格式
|
||||
- password:必填,长度6~128
|
||||
- avatar:可选,URL格式
|
||||
- verification_code:必填,长度6
|
||||
- 响应
|
||||
- 成功:返回包含 token 与用户信息的结构化响应
|
||||
- 失败:返回统一错误响应,包含业务码与消息
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L84)
|
||||
- [common.go](file://internal/types/common.go#L33-L40)
|
||||
- [response.go](file://internal/model/response.go#L20-L73)
|
||||
|
||||
### 验证码机制与防暴力注册
|
||||
- 验证码生成与存储
|
||||
- 生成6位数字验证码,有效期10分钟,发送频率限制1分钟。
|
||||
- 存储于 Redis,键命名包含类型与邮箱。
|
||||
- 验证流程
|
||||
- 校验验证码是否存在且一致,一致即删除该验证码。
|
||||
- 防暴力注册
|
||||
- 发送频率限制键存在即拒绝重复发送,降低刷验证码风险。
|
||||
- 注册接口对用户名/邮箱唯一性检查,避免重复注册。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> Gen["生成6位验证码"]
|
||||
Gen --> Store["写入Redis: 验证码键(10分钟)"]
|
||||
Store --> RateLimit["写入Redis: 发送频率限制键(1分钟)"]
|
||||
RateLimit --> Email["发送邮件"]
|
||||
Email --> Verify["校验验证码"]
|
||||
Verify --> Match{"是否匹配?"}
|
||||
Match --> |否| Expired["返回错误: 验证码错误/过期"]
|
||||
Match --> |是| Del["删除验证码键"]
|
||||
Del --> Done(["结束"])
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L26-L98)
|
||||
|
||||
章节来源
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
|
||||
### 密码加密与存储
|
||||
- 加密方式:bcrypt,默认成本
|
||||
- 存储:用户密码以密文形式保存,不返回明文
|
||||
- 登录校验:使用 bcrypt 对比哈希
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
In(["输入明文密码"]) --> Hash["bcrypt加密"]
|
||||
Hash --> Out(["返回密文"])
|
||||
Out --> Store["持久化存储"]
|
||||
Store --> Login["登录时对比哈希"]
|
||||
Login --> Ok["匹配成功"]
|
||||
Login --> Fail["匹配失败"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
- [user_service.go](file://internal/service/user_service.go#L32-L36)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
|
||||
章节来源
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
- [user_service.go](file://internal/service/user_service.go#L32-L36)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
|
||||
### 用户模型与唯一性约束
|
||||
- 用户模型包含:id、username、password、email、avatar、points、role、status、属性、登录时间、创建/更新时间等
|
||||
- 唯一性约束:username 与 email 在数据库层面唯一索引
|
||||
- 注册时:若用户名或邮箱已存在,返回错误
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER {
|
||||
bigint id PK
|
||||
varchar username UK
|
||||
varchar password
|
||||
varchar email UK
|
||||
varchar avatar
|
||||
integer points
|
||||
varchar role
|
||||
smallint status
|
||||
jsonb properties
|
||||
timestamp last_login_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
|
||||
章节来源
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
- [user_service.go](file://internal/service/user_service.go#L14-L31)
|
||||
|
||||
### 错误码与响应格式
|
||||
- 成功:200,返回结构化响应,包含 token 与用户信息
|
||||
- 参数错误:400,请求体绑定失败或字段校验失败
|
||||
- 验证码错误:400,验证码不存在/过期/不匹配
|
||||
- 资源冲突:409,用户名或邮箱已存在
|
||||
- 服务器错误:500,内部异常
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
|
||||
- [response.go](file://internal/model/response.go#L20-L73)
|
||||
- [user_service.go](file://internal/service/user_service.go#L14-L31)
|
||||
|
||||
### 安全考虑与最佳实践
|
||||
- 密码强度
|
||||
- 至少6位,建议结合复杂度策略(字母、数字、特殊字符)
|
||||
- 使用 bcrypt 存储,避免明文或弱加密
|
||||
- 邮箱格式验证
|
||||
- 使用框架内置邮箱格式校验
|
||||
- 防暴力注册
|
||||
- 验证码发送频率限制(1分钟)
|
||||
- 验证码有效期(10分钟),过期自动失效
|
||||
- 唯一性检查防止重复注册
|
||||
- 传输安全
|
||||
- 建议启用 HTTPS,避免明文传输
|
||||
- 日志与审计
|
||||
- 记录注册失败与异常行为,便于追踪
|
||||
|
||||
章节来源
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L14-L24)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
- [user_service.go](file://internal/service/user_service.go#L14-L31)
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
|
||||
## 依赖关系分析
|
||||
- 处理器依赖
|
||||
- 验证码服务:用于校验注册验证码
|
||||
- 用户服务:执行注册流程(唯一性检查、密码加密、创建用户、生成JWT)
|
||||
- JWT 服务:生成登录令牌
|
||||
- 服务依赖
|
||||
- 密码服务:bcrypt 加密与校验
|
||||
- 数据模型:用户实体
|
||||
- 工具与中间件
|
||||
- 路由:定义 /api/v1/auth/register
|
||||
- 统一响应:封装业务码与消息
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Handler["处理器: Register"] --> VS["验证码服务: VerifyCode"]
|
||||
Handler --> US["用户服务: RegisterUser"]
|
||||
US --> PS["密码服务: HashPassword"]
|
||||
US --> Model["用户模型: User"]
|
||||
US --> JWT["JWT服务: GenerateToken"]
|
||||
Handler --> Resp["统一响应: Response/ErrorResponse"]
|
||||
Route["路由: /api/v1/auth/register"] --> Handler
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L68)
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
- [response.go](file://internal/model/response.go#L20-L73)
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L27-L84)
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L68)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
- [user.go](file://internal/model/user.go#L7-L21)
|
||||
- [response.go](file://internal/model/response.go#L20-L73)
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
|
||||
## 性能与扩展性
|
||||
- 验证码缓存
|
||||
- Redis 存储验证码与频率限制,具备高并发与低延迟特性
|
||||
- 密码加密
|
||||
- bcrypt 默认成本在安全性与性能间平衡,可根据硬件能力调整
|
||||
- 扩展建议
|
||||
- 引入速率限制中间件,对 /api/v1/auth/register 进一步限流
|
||||
- 增加 IP/设备维度的风控策略
|
||||
- 对高频失败场景增加验证码挑战或人机验证
|
||||
|
||||
[本节为通用建议,不直接分析具体文件]
|
||||
|
||||
## 故障排查指南
|
||||
- 常见错误与定位
|
||||
- 参数错误:检查请求体字段是否满足长度/格式要求
|
||||
- 验证码错误:确认验证码是否过期、是否与发送邮箱一致
|
||||
- 用户名/邮箱已存在:检查数据库唯一性约束
|
||||
- 单元测试参考
|
||||
- 注册请求体校验、错误处理与响应格式可通过测试用例验证
|
||||
|
||||
章节来源
|
||||
- [auth_handler_test.go](file://internal/handler/auth_handler_test.go#L74-L116)
|
||||
- [common_test.go](file://internal/types/common_test.go#L215-L287)
|
||||
|
||||
## 结论
|
||||
用户注册 API 通过严格的请求体校验、验证码机制与 bcrypt 密码加密,提供了基础的安全保障。配合唯一性检查与频率限制,能够有效降低重复注册与暴力注册的风险。建议在生产环境中进一步完善风控策略与监控告警体系。
|
||||
|
||||
[本节为总结性内容,不直接分析具体文件]
|
||||
|
||||
## 附录
|
||||
|
||||
### 请求与响应示例(路径引用)
|
||||
- 请求体字段与校验规则
|
||||
- 字段定义与校验规则参见:[请求体定义](file://internal/types/common.go#L33-L40)
|
||||
- 成功响应结构
|
||||
- 成功响应结构参见:[统一响应](file://internal/model/response.go#L20-L73)
|
||||
- 登录响应结构参见:[登录响应](file://internal/types/common.go#L107-L126)
|
||||
- 错误响应结构
|
||||
- 错误响应结构参见:[错误响应](file://internal/model/response.go#L20-L73)
|
||||
- 路由与端点
|
||||
- 注册路由参见:[路由定义](file://internal/handler/routes.go#L16-L26)
|
||||
- 处理器与业务流程
|
||||
- 注册处理器参见:[注册处理器](file://internal/handler/auth_handler.go#L27-L84)
|
||||
- 注册业务流程参见:[注册服务](file://internal/service/user_service.go#L12-L68)
|
||||
- 验证码与防暴力
|
||||
- 验证码生成与校验参见:[验证码服务](file://internal/service/verification_service.go#L26-L98)
|
||||
- 密码加密
|
||||
- 密码加密与校验参见:[密码服务](file://pkg/auth/password.go#L7-L21)
|
||||
@@ -1,351 +0,0 @@
|
||||
# 用户登录
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [password.go](file://pkg/auth/password.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [user.go](file://internal/model/user.go)
|
||||
- [token.go](file://internal/model/token.go)
|
||||
- [token_service.go](file://internal/service/token_service.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向“用户登录”API,围绕 /api/v1/auth/login 端点进行完整说明,覆盖:
|
||||
- HTTP 方法与路由
|
||||
- 请求体结构(用户名或邮箱登录)
|
||||
- 响应格式(包含JWT token与用户信息)
|
||||
- 错误码与错误处理
|
||||
- JWT令牌生成流程(访问令牌与声明)
|
||||
- 登录成功/失败后的审计日志记录(IP、User-Agent)
|
||||
- 安全建议(防暴力破解、错误提示模糊化)
|
||||
|
||||
## 项目结构
|
||||
该登录流程涉及以下模块协作:
|
||||
- 路由层:注册 /api/v1/auth/login 路由
|
||||
- 控制器层:处理登录请求、参数校验、调用服务层
|
||||
- 服务层:用户查找、密码校验、令牌生成、登录日志记录
|
||||
- 认证与密码:JWT服务、bcrypt密码加解密
|
||||
- 数据模型:用户、登录日志、令牌
|
||||
- 中间件:JWT认证中间件用于后续受保护接口
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["客户端"] --> Routes["路由注册<br/>/api/v1/auth/login"]
|
||||
Routes --> Handler["控制器 Login()<br/>参数绑定/错误处理"]
|
||||
Handler --> Service["服务层 LoginUser()<br/>查找用户/校验密码/生成JWT"]
|
||||
Service --> JWT["JWT服务 GenerateToken()<br/>签发访问令牌"]
|
||||
Service --> ModelUser["用户模型 User"]
|
||||
Service --> ModelLog["登录日志 UserLoginLog"]
|
||||
Handler --> Resp["统一响应 Response/ErrorResponse"]
|
||||
Client --> Middleware["认证中间件 AuthMiddleware()<br/>Bearer Token 校验"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L25)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
|
||||
- [user_service.go](file://internal/service/user_service.go#L70-L122)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
|
||||
- [user.go](file://internal/model/user.go#L1-L21)
|
||||
- [user.go](file://internal/model/user.go#L52-L71)
|
||||
- [auth.go](file://internal/middleware/auth.go#L12-L56)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L25)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
|
||||
|
||||
## 核心组件
|
||||
- 路由与控制器
|
||||
- /api/v1/auth/login 由控制器 Login 处理,负责参数绑定、IP/User-Agent采集、调用服务层并返回统一响应。
|
||||
- 服务层
|
||||
- LoginUser 支持用户名或邮箱登录;校验用户状态与密码;生成JWT;更新最后登录时间;记录成功/失败日志。
|
||||
- 认证与密码
|
||||
- JWT服务提供 GenerateToken/ValidateToken;密码使用 bcrypt 进行哈希与校验。
|
||||
- 数据模型
|
||||
- User:用户信息;UserLoginLog:登录审计日志;Token:令牌模型(用于后续刷新/失效等场景)。
|
||||
- 统一响应
|
||||
- Response/ErrorResponse 提供统一的状态码与消息封装。
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
|
||||
- [user_service.go](file://internal/service/user_service.go#L70-L122)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
|
||||
- [password.go](file://pkg/auth/password.go#L7-L21)
|
||||
- [user.go](file://internal/model/user.go#L1-L21)
|
||||
- [user.go](file://internal/model/user.go#L52-L71)
|
||||
- [token.go](file://internal/model/token.go#L1-L15)
|
||||
- [response.go](file://internal/model/response.go#L20-L53)
|
||||
|
||||
## 架构总览
|
||||
登录端到端序列图(POST /api/v1/auth/login):
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant R as "路由"
|
||||
participant H as "控制器 Login()"
|
||||
participant S as "服务层 LoginUser()"
|
||||
participant J as "JWT服务"
|
||||
participant U as "用户模型"
|
||||
participant L as "登录日志"
|
||||
C->>R : "POST /api/v1/auth/login"
|
||||
R->>H : "进入 Login 控制器"
|
||||
H->>H : "ShouldBindJSON 绑定请求体"
|
||||
H->>S : "LoginUser(jwtService, username, password, ip, ua)"
|
||||
S->>U : "根据用户名或邮箱查找用户"
|
||||
S->>S : "校验用户状态"
|
||||
S->>S : "bcrypt 校验密码"
|
||||
S->>J : "GenerateToken(userID, username, role)"
|
||||
J-->>S : "返回JWT字符串"
|
||||
S->>U : "更新 last_login_at"
|
||||
S->>L : "logSuccessLogin/logFailedLogin"
|
||||
S-->>H : "返回 user, token"
|
||||
H-->>C : "200 成功响应或401失败响应"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L86-L147)
|
||||
- [user_service.go](file://internal/service/user_service.go#L70-L122)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L32-L53)
|
||||
- [user.go](file://internal/model/user.go#L1-L21)
|
||||
- [user.go](file://internal/model/user.go#L52-L71)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 接口定义与请求/响应
|
||||
- HTTP 方法与路径
|
||||
- 方法:POST
|
||||
- 路径:/api/v1/auth/login
|
||||
- 请求体结构
|
||||
- 字段:username(支持用户名或邮箱)、password(6-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:支持用户名或邮箱
|
||||
- password:6-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 与 userInfo(id、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
|
||||
- 成功响应
|
||||
- code:200
|
||||
- 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)
|
||||
@@ -1,460 +0,0 @@
|
||||
# 认证API
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [password.go](file://pkg/auth/password.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [verification_service.go](file://internal/service/verification_service.go)
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [email.go](file://pkg/email/email.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者,系统性梳理认证API的设计与实现,覆盖用户注册、登录、发送验证码与重置密码四个核心端点。文档基于路由分组与处理器实现,详细说明每个API的HTTP方法、请求参数、响应格式与错误码;重点解释JWT认证机制在登录流程中的作用,以及验证码服务与邮箱服务的集成方式;并提供实际请求/响应示例路径,帮助快速理解认证流程。同时总结安全要点,包括密码加密存储、JWT过期策略与防暴力破解措施。
|
||||
|
||||
## 项目结构
|
||||
认证相关能力由“路由层-处理器层-服务层-基础设施层”四层构成:
|
||||
- 路由层:在统一的路由组下挂载认证相关端点
|
||||
- 处理器层:负责参数绑定、调用服务、组织响应与错误处理
|
||||
- 服务层:封装业务逻辑(用户注册/登录、验证码发送与校验、密码变更等)
|
||||
- 基础设施层:JWT签发与校验、密码加解密、Redis验证码存储、SMTP邮件发送
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由层"
|
||||
R["routes.go<br/>注册/auth路由组"]
|
||||
end
|
||||
subgraph "处理器层"
|
||||
AH["auth_handler.go<br/>注册/登录/发送验证码/重置密码"]
|
||||
CH["captcha_handler.go<br/>图形验证码生成/校验"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
US["user_service.go<br/>注册/登录/密码重置等"]
|
||||
VS["verification_service.go<br/>验证码生成/发送/校验"]
|
||||
CS["captcha_service.go<br/>图形验证码生成/校验"]
|
||||
end
|
||||
subgraph "基础设施层"
|
||||
JWT["jwt.go<br/>JWT签发/校验"]
|
||||
PW["password.go<br/>bcrypt加解密"]
|
||||
EM["email.go<br/>SMTP邮件发送"]
|
||||
CFG["config.go<br/>JWT/邮件/Redis等配置"]
|
||||
end
|
||||
R --> AH
|
||||
R --> CH
|
||||
AH --> US
|
||||
AH --> VS
|
||||
CH --> CS
|
||||
US --> JWT
|
||||
US --> PW
|
||||
VS --> EM
|
||||
VS --> CFG
|
||||
CS --> CFG
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L25)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L17-L249)
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L122)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L14-L119)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L18-L166)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L10-L71)
|
||||
- [password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [email.go](file://pkg/email/email.go#L15-L163)
|
||||
- [config.go](file://pkg/config/config.go#L67-L107)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L16-L25)
|
||||
|
||||
## 核心组件
|
||||
- 路由组与端点
|
||||
- /api/v1/auth/register:POST,注册
|
||||
- /api/v1/auth/login:POST,登录
|
||||
- /api/v1/auth/send-code:POST,发送验证码
|
||||
- /api/v1/auth/reset-password:POST,重置密码
|
||||
- 中间件
|
||||
- 认证中间件:对受保护资源进行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)
|
||||
@@ -1,343 +0,0 @@
|
||||
# 重置密码
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go)
|
||||
- [internal/types/common.go](file://internal/types/common.go)
|
||||
- [internal/model/response.go](file://internal/model/response.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者与测试人员,系统化梳理“重置密码”API的完整实现与使用规范,覆盖以下要点:
|
||||
- 接口定位与HTTP方法:POST /api/v1/auth/reset-password
|
||||
- 请求体字段与校验规则:email、verificationCode、newPassword
|
||||
- 响应格式与错误码:统一的业务状态码与错误消息
|
||||
- 流程说明:验证码验证 → 密码加密更新 → 成功响应
|
||||
- 安全考虑:验证码一次性使用、有效期控制、密码复杂度要求
|
||||
|
||||
## 项目结构
|
||||
围绕“重置密码”的关键文件分布如下:
|
||||
- 路由注册:在路由组中将 /api/v1/auth/reset-password 绑定到处理器
|
||||
- 处理器:接收请求、绑定参数、调用服务层、返回统一响应
|
||||
- 服务层:
|
||||
- 验证码服务:生成、发送、验证、删除;验证码一次性使用与有效期控制
|
||||
- 用户服务:根据邮箱查找用户并更新密码(密码经加密后持久化)
|
||||
- 工具与模型:
|
||||
- 密码工具:bcrypt 加密与校验
|
||||
- 请求类型:ResetPasswordRequest 的字段与约束
|
||||
- 响应模型:统一响应结构与常用状态码
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "接口层"
|
||||
R["路由注册<br/>/api/v1/auth/reset-password"]
|
||||
H["处理器<br/>ResetPassword"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
VS["验证码服务<br/>VerifyCode/Generate/Send/Delete"]
|
||||
US["用户服务<br/>ResetUserPassword"]
|
||||
end
|
||||
subgraph "工具与模型"
|
||||
PW["密码工具<br/>bcrypt 加密/校验"]
|
||||
RT["请求类型<br/>ResetPasswordRequest"]
|
||||
RM["响应模型<br/>统一响应/状态码"]
|
||||
end
|
||||
R --> H
|
||||
H --> VS
|
||||
H --> US
|
||||
US --> PW
|
||||
H --> RM
|
||||
VS --> RM
|
||||
RT --> H
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L98)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L166-L184)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L55-L61)
|
||||
- [internal/model/response.go](file://internal/model/response.go#L1-L86)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go#L16-L26)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L194-L249)
|
||||
|
||||
## 核心组件
|
||||
- 接口路径与方法
|
||||
- 方法:POST
|
||||
- 路径:/api/v1/auth/reset-password
|
||||
- 请求体结构
|
||||
- 字段:
|
||||
- email:字符串,必填,需符合邮箱格式
|
||||
- verificationCode:字符串,必填,长度必须为6
|
||||
- newPassword:字符串,必填,长度范围为6~128
|
||||
- 校验来源:请求类型定义与处理器绑定校验
|
||||
- 响应格式
|
||||
- 成功:统一响应结构,code=200,message=“操作成功”,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=200,message=“操作成功”
|
||||
- 失败:code=400或500,message=错误描述
|
||||
|
||||
章节来源
|
||||
- [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 → HashPassword(bcrypt加密)
|
||||
- 服务层到存储
|
||||
- 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)
|
||||
- Status:200
|
||||
- Body:
|
||||
- code:200
|
||||
- message:“操作成功”
|
||||
- data:空对象或简要提示
|
||||
- 失败响应示例(JSON)
|
||||
- Status:400 或 500
|
||||
- Body:
|
||||
- code:400 或 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)
|
||||
@@ -1,294 +0,0 @@
|
||||
# 验证码API
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go)
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
- [response.go](file://internal/model/response.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [captcha_handler_test.go](file://internal/handler/captcha_handler_test.go)
|
||||
- [captcha_service_test.go](file://internal/service/captcha_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向验证码服务API的使用者与维护者,围绕路由组“/api/v1/captcha”下的两个核心端点:
|
||||
- GET /generate:生成图形验证码(滑块拼图),返回主图、滑块图、验证码唯一标识及目标Y坐标等信息。
|
||||
- POST /verify:验证用户提交的滑动偏移量,判断是否在容差范围内,并在验证通过后清理缓存。
|
||||
|
||||
文档将详细说明验证码的生成机制(基于滑块拼图算法)、有效期管理(Redis过期策略)、验证流程与状态处理,并给出请求/响应示例。同时,说明该服务如何与注册、登录、更换邮箱等功能集成以提升安全性。
|
||||
|
||||
## 项目结构
|
||||
验证码API位于路由组“/api/v1/captcha”,由处理器与服务层共同实现,底层依赖Redis进行验证码数据的临时存储与过期控制。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "路由层"
|
||||
R["routes.go<br/>注册 /api/v1/captcha/* 路由"]
|
||||
end
|
||||
subgraph "处理器层"
|
||||
GH["captcha_handler.go<br/>Generate/Verify"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
CS["captcha_service.go<br/>GenerateCaptchaData/VerifyCaptchaData"]
|
||||
end
|
||||
subgraph "基础设施"
|
||||
RC["redis.go<br/>Client 封装"]
|
||||
end
|
||||
R --> GH
|
||||
GH --> CS
|
||||
CS --> RC
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [routes.go](file://internal/handler/routes.go#L80-L86)
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L80-L86)
|
||||
|
||||
## 核心组件
|
||||
- 路由注册:在路由组“/api/v1/captcha”下注册“/generate”和“/verify”两个端点。
|
||||
- 处理器:负责参数绑定、调用服务层、组织响应。
|
||||
- 服务层:负责验证码生成(滑块拼图)、目标坐标提取、Redis存储与过期设置、验证偏移量、容差判断、验证后清理。
|
||||
- Redis客户端:提供Set/Get/Del等基础能力,封装错误处理。
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L80-L86)
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L1-L77)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
## 架构总览
|
||||
验证码API采用“路由 -> 处理器 -> 服务 -> Redis”的分层设计。生成阶段将目标坐标与容差信息写入Redis并设置过期时间;验证阶段读取目标坐标,计算容差并判定有效性,通过后立即删除对应键,防止重复使用。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant G as "Generate 处理器"
|
||||
participant S as "验证码服务"
|
||||
participant R as "Redis 客户端"
|
||||
C->>G : GET /api/v1/captcha/generate
|
||||
G->>S : GenerateCaptchaData(ctx, redisClient)
|
||||
S->>S : 生成滑块拼图数据
|
||||
S->>R : Set(key=captcha : id, value={tx,ty}, expire=5m)
|
||||
S-->>G : 返回 masterImage, tileImage, captchaId, y
|
||||
G-->>C : JSON {code,data{masterImage,tileImage,captchaId,y}}
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L12-L34)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L76-L135)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
|
||||
章节来源
|
||||
- [captcha_handler.go](file://internal/handler/captcha_handler.go#L12-L34)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L76-L135)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 路由与端点
|
||||
- 路由组“/api/v1/captcha”
|
||||
- GET /generate:生成验证码数据(主图、滑块图、验证码ID、目标Y坐标)。
|
||||
- POST /verify:验证用户提交的滑动偏移量。
|
||||
|
||||
章节来源
|
||||
- [routes.go](file://internal/handler/routes.go#L80-L86)
|
||||
|
||||
### 生成验证码(GET /generate)
|
||||
- 输入:无显式请求体。
|
||||
- 输出:包含以下字段的数据对象
|
||||
- masterImage:主图(base64字符串)
|
||||
- tileImage:滑块图(base64字符串)
|
||||
- captchaId:验证码唯一标识(用于后续验证)
|
||||
- y:目标Y坐标(前端可据此定位滑块初始位置)
|
||||
- 内部流程要点
|
||||
- 生成唯一ID作为验证码会话标识。
|
||||
- 生成滑块拼图数据,提取目标X/Y坐标。
|
||||
- 将目标坐标序列化后写入Redis,键名前缀为“captcha:”,过期时间为300秒(5分钟)。
|
||||
- 返回主图、滑块图、captchaId与修正后的Y坐标给前端。
|
||||
|
||||
请求/响应示例(路径)
|
||||
- 请求:GET /api/v1/captcha/generate
|
||||
- 响应:JSON
|
||||
- code:200
|
||||
- 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)
|
||||
@@ -1,220 +0,0 @@
|
||||
# Redis缓存集成
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
- [manager.go](file://pkg/redis/manager.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go)
|
||||
- [verification_service.go](file://internal/service/verification_service.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [项目结构](#项目结构)
|
||||
2. [Redis客户端封装设计](#redis客户端封装设计)
|
||||
3. [连接池与健康检查机制](#连接池与健康检查机制)
|
||||
4. [缓存操作实现原理](#缓存操作实现原理)
|
||||
5. [JWT令牌存储应用](#jwt令牌存储应用)
|
||||
6. [会话管理与验证码应用](#会话管理与验证码应用)
|
||||
7. [限流控制实现](#限流控制实现)
|
||||
8. [缓存问题预防策略](#缓存问题预防策略)
|
||||
9. [高可用架构支持](#高可用架构支持)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "pkg"
|
||||
Redis[redis]
|
||||
Config[config]
|
||||
Auth[auth]
|
||||
end
|
||||
subgraph "internal"
|
||||
Service[service]
|
||||
Repository[repository]
|
||||
end
|
||||
Redis --> Service
|
||||
Config --> Redis
|
||||
Auth --> Service
|
||||
Service --> Repository
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L1-L166)
|
||||
|
||||
## Redis客户端封装设计
|
||||
|
||||
CarrotSkin项目中的Redis客户端封装采用分层设计模式,通过`Client`结构体对`github.com/redis/go-redis/v9`库进行包装,提供更简洁的API接口。`Client`结构体包含`*redis.Client`指针和`*zap.Logger`日志记录器,实现了日志记录和错误处理的统一管理。
|
||||
|
||||
封装设计遵循单一职责原则,将Redis连接管理、操作执行和日志记录分离。通过`New`函数创建客户端实例时,会根据配置参数初始化连接,并执行连接测试,确保连接的可用性。这种设计模式提高了代码的可维护性和可测试性。
|
||||
|
||||
**本节来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L15-L52)
|
||||
- [manager.go](file://pkg/redis/manager.go#L11-L18)
|
||||
|
||||
## 连接池与健康检查机制
|
||||
|
||||
Redis连接池配置在`RedisConfig`结构体中定义,包含`Host`、`Port`、`Password`、`Database`和`PoolSize`等关键参数。连接池大小通过`PoolSize`字段配置,默认值为10,可根据实际负载情况进行调整。
|
||||
|
||||
健康检查机制在客户端初始化时实现,通过`Ping`命令测试连接可用性。使用`context.WithTimeout`设置5秒超时,防止连接测试阻塞主线程。连接成功后会记录包含主机、端口和数据库信息的日志,便于监控和故障排查。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as 应用程序
|
||||
participant Manager as Redis管理器
|
||||
participant Client as Redis客户端
|
||||
participant Redis as Redis服务器
|
||||
App->>Manager : Init(cfg, logger)
|
||||
Manager->>Client : New(cfg, logger)
|
||||
Client->>Redis : Ping()
|
||||
Redis-->>Client : PONG
|
||||
Client-->>Manager : 返回客户端实例
|
||||
Manager-->>App : 初始化完成
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L22-L47)
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
|
||||
**本节来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L22-L47)
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
|
||||
## 缓存操作实现原理
|
||||
|
||||
缓存操作通过`Client`结构体的方法实现,包括`Set`、`Get`、`Del`、`Exists`、`Expire`等基本操作。这些方法直接代理到底层`redis.Client`的对应方法,保持了与原生API的一致性。
|
||||
|
||||
`Set`方法接受`context.Context`、键、值和过期时间参数,支持设置键值对及其过期时间。`Get`方法返回字符串值和错误,通过`Nil`方法可以判断键不存在的情况。`Expire`方法用于修改现有键的过期时间,支持动态调整缓存策略。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Client {
|
||||
+*redis.Client
|
||||
+*zap.Logger
|
||||
+Set(ctx, key, value, expiration) error
|
||||
+Get(ctx, key) (string, error)
|
||||
+Del(ctx, keys) error
|
||||
+Exists(ctx, keys) (int64, error)
|
||||
+Expire(ctx, key, expiration) error
|
||||
+Nil(err) bool
|
||||
}
|
||||
class RedisConfig {
|
||||
+Host string
|
||||
+Port int
|
||||
+Password string
|
||||
+Database int
|
||||
+PoolSize int
|
||||
}
|
||||
Client --> RedisConfig : 使用
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
|
||||
**本节来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L83)
|
||||
|
||||
## JWT令牌存储应用
|
||||
|
||||
JWT令牌存储利用Redis作为临时存储介质,通过`Set`方法将令牌与用户信息关联存储。令牌的过期时间与JWT的有效期保持一致,确保令牌在Redis中的生命周期与JWT一致。
|
||||
|
||||
在`jwt.go`中,JWT服务生成令牌后,可以通过Redis存储令牌的元数据,如用户ID、角色等信息。这种设计实现了无状态认证与有状态缓存的结合,既保持了JWT的无状态特性,又可以通过Redis快速验证令牌的有效性。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户
|
||||
participant Auth as 认证服务
|
||||
participant Redis as Redis缓存
|
||||
User->>Auth : 登录请求
|
||||
Auth->>Auth : 生成JWT令牌
|
||||
Auth->>Redis : Set(token, userInfo, expireTime)
|
||||
Redis-->>Auth : 存储成功
|
||||
Auth-->>User : 返回JWT令牌
|
||||
User->>Auth : 带JWT的请求
|
||||
Auth->>Redis : Get(token)
|
||||
Redis-->>Auth : 返回用户信息
|
||||
Auth-->>User : 处理请求
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L32-L52)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L63)
|
||||
|
||||
**本节来源**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L32-L52)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L63)
|
||||
|
||||
## 会话管理与验证码应用
|
||||
|
||||
会话管理通过Redis存储会话数据,每个会话以唯一ID作为键,会话数据作为值进行存储。验证码应用在`captcha_service.go`中实现,使用`redisKeyPrefix = "captcha:"`作为键前缀,确保验证码数据的隔离性。
|
||||
|
||||
验证码生成时,将滑块的目标坐标等验证信息序列化后存储到Redis,设置300秒过期时间。验证时从Redis获取原始数据,与用户输入进行比对,验证成功后立即删除Redis记录,防止重复使用。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([生成验证码]) --> Generate["生成滑块坐标(Tx,Ty)"]
|
||||
Generate --> Serialize["序列化为JSON"]
|
||||
Serialize --> Store["Set(captcha:id, json, 300s)"]
|
||||
Store --> Return["返回验证码数据"]
|
||||
Verify([验证验证码]) --> Get["Get(captcha:id)"]
|
||||
Get --> Check{"是否存在?"}
|
||||
Check --> |否| Expired["返回: 验证码已过期"]
|
||||
Check --> |是| Parse["解析JSON数据"]
|
||||
Parse --> Validate["验证用户输入"]
|
||||
Validate --> Delete["Del(captcha:id)"]
|
||||
Delete --> Result["返回验证结果"]
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L75-L135)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L63)
|
||||
|
||||
**本节来源**
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L75-L135)
|
||||
- [redis.go](file://pkg/redis/redis.go#L144-L146)
|
||||
|
||||
## 限流控制实现
|
||||
|
||||
限流控制在`verification_service.go`中实现,通过Redis的`Set`命令设置频率限制。使用`codeType`和`email`组合生成限流键,值设为"1",过期时间由`CodeRateLimit`常量定义。
|
||||
|
||||
当用户请求发送验证码时,先检查限流键是否存在,若存在则拒绝请求,防止频繁发送。这种基于Redis的限流机制简单高效,能够有效防止恶意刷屏攻击,保护系统资源。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Request[发送验证码请求] --> Check["Exists(verification:rate:email)"]
|
||||
Check --> |存在| Reject[拒绝请求]
|
||||
Check --> |不存在| Send[发送验证码]
|
||||
Send --> Set["Set(verification:rate:email, 1, 60s)"]
|
||||
Set --> Response[返回响应]
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L58-L67)
|
||||
- [redis.go](file://pkg/redis/redis.go#L75-L78)
|
||||
|
||||
**本节来源**
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L58-L67)
|
||||
|
||||
## 缓存问题预防策略
|
||||
|
||||
针对缓存穿透、雪崩、击穿问题,CarrotSkin采用多种预防策略。对于缓存穿透,使用`Nil`方法识别空值情况,避免频繁查询数据库。对于缓存雪崩,建议在配置中设置随机的过期时间偏移,避免大量缓存同时失效。
|
||||
|
||||
缓存击穿问题通过互斥锁或逻辑过期策略解决。当热点数据失效时,只允许一个请求加载数据,其他请求等待并使用旧数据,直到新数据加载完成。这种策略既保证了数据的一致性,又避免了数据库的瞬时压力。
|
||||
|
||||
**本节来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L160-L162)
|
||||
- [captcha_service.go](file://internal/service/captcha_service.go#L144-L145)
|
||||
|
||||
## 高可用架构支持
|
||||
|
||||
Redis高可用架构支持通过配置文件中的连接参数实现。虽然当前代码主要针对单机模式,但`github.com/redis/go-redis/v9`库原生支持哨兵模式和集群模式。通过修改配置,可以无缝切换到高可用架构。
|
||||
|
||||
连接故障恢复机制内置于客户端库中,自动处理网络抖动和临时故障。建议在生产环境中配置合理的超时时间和重试策略,确保系统的稳定性和可靠性。监控连接状态和性能指标,及时发现和解决潜在问题。
|
||||
|
||||
**本节来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L24-L32)
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
@@ -1,314 +0,0 @@
|
||||
# 外部集成
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [email.go](file://pkg/email/email.go)
|
||||
- [manager.go](file://pkg/email/manager.go)
|
||||
- [minio.go](file://pkg/storage/minio.go)
|
||||
- [storage_manager.go](file://pkg/storage/manager.go)
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
- [redis_manager.go](file://pkg/redis/manager.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [manager.go](file://pkg/config/manager.go)
|
||||
- [verification_service.go](file://internal/service/verification_service.go)
|
||||
- [upload_service.go](file://internal/service/upload_service.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [邮件服务集成](#邮件服务集成)
|
||||
3. [对象存储集成](#对象存储集成)
|
||||
4. [缓存系统集成](#缓存系统集成)
|
||||
5. [集成初始化流程](#集成初始化流程)
|
||||
6. [故障排除与性能调优](#故障排除与性能调优)
|
||||
7. [总结](#总结)
|
||||
|
||||
## 简介
|
||||
CarrotSkin项目通过集成第三方服务来实现关键功能,包括邮件服务(SMTP)、对象存储(MinIO/RustFS)和缓存系统(Redis)。这些集成分别用于发送验证邮件、存储用户上传的皮肤文件以及缓存会话数据和验证码。本文档详细说明这些外部服务的配置、初始化和使用方式,为初学者提供配置示例,同时为经验丰富的开发者提供故障排除和性能调优的高级技巧。
|
||||
|
||||
## 邮件服务集成
|
||||
|
||||
邮件服务用于向用户发送验证码邮件,包括注册验证、密码重置和更换邮箱等场景。该服务通过SMTP协议与邮件服务器通信,支持SSL/TLS加密。
|
||||
|
||||
### 配置与初始化
|
||||
邮件服务的配置通过环境变量进行管理,主要配置项包括SMTP主机、端口、用户名、密码和发件人名称。服务采用单例模式初始化,确保线程安全。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class EmailConfig {
|
||||
+bool Enabled
|
||||
+string SMTPHost
|
||||
+int SMTPPort
|
||||
+string Username
|
||||
+string Password
|
||||
+string FromName
|
||||
}
|
||||
class Service {
|
||||
-EmailConfig cfg
|
||||
-*zap.Logger logger
|
||||
+SendVerificationCode(to, code, purpose) error
|
||||
+SendResetPassword(to, code) error
|
||||
+SendEmailVerification(to, code) error
|
||||
+SendChangeEmail(to, code) error
|
||||
}
|
||||
class Manager {
|
||||
+Init(cfg EmailConfig, logger *zap.Logger) error
|
||||
+GetService() (*Service, error)
|
||||
+MustGetService() *Service
|
||||
}
|
||||
EmailConfig --> Service : "配置"
|
||||
Service --> Manager : "实现"
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L98-L106)
|
||||
- [email.go](file://pkg/email/email.go#L16-L19)
|
||||
- [manager.go](file://pkg/email/manager.go#L11-L18)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L98-L106)
|
||||
- [email.go](file://pkg/email/email.go#L16-L19)
|
||||
- [manager.go](file://pkg/email/manager.go#L11-L18)
|
||||
|
||||
### 使用方式
|
||||
邮件服务提供了多种发送验证码的方法,根据不同的业务场景调用相应的函数。例如,`SendEmailVerification`用于发送邮箱验证邮件,`SendResetPassword`用于发送密码重置邮件。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Handler as "Handler"
|
||||
participant Service as "VerificationService"
|
||||
participant EmailService as "EmailService"
|
||||
Handler->>Service : SendVerificationCode()
|
||||
Service->>Service : GenerateVerificationCode()
|
||||
Service->>Service : 存储验证码到Redis
|
||||
Service->>EmailService : SendEmailVerification()
|
||||
EmailService->>EmailService : 构建邮件内容
|
||||
EmailService->>SMTP : 发送邮件
|
||||
SMTP-->>EmailService : 发送结果
|
||||
EmailService-->>Service : 发送结果
|
||||
Service-->>Handler : 结果
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
|
||||
- [email.go](file://pkg/email/email.go#L29-L55)
|
||||
|
||||
**Section sources**
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
|
||||
- [email.go](file://pkg/email/email.go#L29-L55)
|
||||
|
||||
## 对象存储集成
|
||||
|
||||
对象存储服务用于存储用户上传的皮肤文件和头像,支持S3兼容的存储系统,如MinIO和RustFS。该服务提供预签名URL功能,允许客户端直接上传文件到存储服务器。
|
||||
|
||||
### 配置与初始化
|
||||
对象存储的配置包括端点地址、访问密钥、密钥和是否使用SSL。存储桶名称通过环境变量配置,支持多个存储桶。服务同样采用单例模式初始化。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class RustFSConfig {
|
||||
+string Endpoint
|
||||
+string AccessKey
|
||||
+string SecretKey
|
||||
+bool UseSSL
|
||||
+map[string]string Buckets
|
||||
}
|
||||
class StorageClient {
|
||||
-*minio.Client client
|
||||
-map[string]string buckets
|
||||
+GetBucket(name) (string, error)
|
||||
+GeneratePresignedURL(ctx, bucket, object, expires) (string, error)
|
||||
+GeneratePresignedPostURL(ctx, bucket, object, minSize, maxSize, expires, useSSL, endpoint) (*PresignedPostPolicyResult, error)
|
||||
}
|
||||
class Manager {
|
||||
+Init(cfg RustFSConfig) error
|
||||
+GetClient() (*StorageClient, error)
|
||||
+MustGetClient() *StorageClient
|
||||
}
|
||||
RustFSConfig --> StorageClient : "配置"
|
||||
StorageClient --> Manager : "实现"
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L58-L64)
|
||||
- [minio.go](file://pkg/storage/minio.go#L15-L18)
|
||||
- [manager.go](file://pkg/storage/manager.go#L9-L16)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L58-L64)
|
||||
- [minio.go](file://pkg/storage/minio.go#L15-L18)
|
||||
- [manager.go](file://pkg/storage/manager.go#L9-L16)
|
||||
|
||||
### 使用方式
|
||||
对象存储服务通过生成预签名URL来实现文件上传。客户端获取URL后,可以直接上传文件,无需经过应用服务器。这提高了上传效率并减少了服务器负载。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Handler as "Handler"
|
||||
participant Service as "UploadService"
|
||||
participant Storage as "StorageClient"
|
||||
Client->>Handler : 请求上传URL
|
||||
Handler->>Service : GenerateAvatarUploadURL()
|
||||
Service->>Storage : GeneratePresignedPostURL()
|
||||
Storage-->>Service : 预签名URL和表单数据
|
||||
Service-->>Handler : 上传配置
|
||||
Handler-->>Client : 上传配置
|
||||
Client->>Storage : 使用预签名URL上传文件
|
||||
Storage-->>Client : 上传结果
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L78-L115)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L120)
|
||||
|
||||
**Section sources**
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L78-L115)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L120)
|
||||
|
||||
## 缓存系统集成
|
||||
|
||||
缓存系统使用Redis来存储临时数据,如验证码、会话信息和频率限制。Redis提供了高性能的键值存储,支持多种数据结构。
|
||||
|
||||
### 配置与初始化
|
||||
Redis的配置包括主机地址、端口、密码、数据库编号和连接池大小。服务采用单例模式初始化,确保线程安全。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class RedisConfig {
|
||||
+string Host
|
||||
+int Port
|
||||
+string Password
|
||||
+int Database
|
||||
+int PoolSize
|
||||
}
|
||||
class Client {
|
||||
-*redis.Client Client
|
||||
-*zap.Logger logger
|
||||
+Set(ctx, key, value, expiration) error
|
||||
+Get(ctx, key) (string, error)
|
||||
+Del(ctx, keys) error
|
||||
+Exists(ctx, keys) (int64, error)
|
||||
+Expire(ctx, key, expiration) error
|
||||
+Incr(ctx, key) (int64, error)
|
||||
+Decr(ctx, key) (int64, error)
|
||||
+HSet(ctx, key, values) error
|
||||
+HGet(ctx, key, field) (string, error)
|
||||
+HGetAll(ctx, key) (map[string]string, error)
|
||||
+HDel(ctx, key, fields) error
|
||||
+SAdd(ctx, key, members) error
|
||||
+SMembers(ctx, key) ([]string, error)
|
||||
+SRem(ctx, key, members) error
|
||||
+SIsMember(ctx, key, member) (bool, error)
|
||||
+ZAdd(ctx, key, members) error
|
||||
+ZRange(ctx, key, start, stop) ([]string, error)
|
||||
+ZRem(ctx, key, members) error
|
||||
+Pipeline() redis.Pipeliner
|
||||
+TxPipeline() redis.Pipeliner
|
||||
+Nil(err) bool
|
||||
+GetBytes(ctx, key) ([]byte, error)
|
||||
}
|
||||
class Manager {
|
||||
+Init(cfg RedisConfig, logger *zap.Logger) error
|
||||
+GetClient() (*Client, error)
|
||||
+MustGetClient() *Client
|
||||
}
|
||||
RedisConfig --> Client : "配置"
|
||||
Client --> Manager : "实现"
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
- [redis.go](file://pkg/redis/redis.go#L16-L19)
|
||||
- [manager.go](file://pkg/redis/manager.go#L11-L18)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
- [redis.go](file://pkg/redis/redis.go#L16-L19)
|
||||
- [manager.go](file://pkg/redis/manager.go#L11-L18)
|
||||
|
||||
### 使用方式
|
||||
缓存系统主要用于存储验证码和频率限制。当用户请求发送验证码时,系统会先检查频率限制,然后生成验证码并存储到Redis中,最后发送邮件。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Handler as "Handler"
|
||||
participant Service as "VerificationService"
|
||||
participant Redis as "RedisClient"
|
||||
Handler->>Service : SendVerificationCode()
|
||||
Service->>Redis : Exists(verification : rate_limit : *)
|
||||
Redis-->>Service : 结果
|
||||
alt 频率限制未超
|
||||
Service->>Service : GenerateVerificationCode()
|
||||
Service->>Redis : Set(verification : code : *, code, expiration)
|
||||
Service->>Redis : Set(verification : rate_limit : *, 1, rateLimit)
|
||||
Service->>EmailService : SendEmailVerification()
|
||||
EmailService-->>Service : 发送结果
|
||||
Service-->>Handler : 结果
|
||||
else 频率限制已超
|
||||
Service-->>Handler : 错误
|
||||
end
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L67)
|
||||
|
||||
**Section sources**
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L76)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L67)
|
||||
|
||||
## 集成初始化流程
|
||||
|
||||
所有外部集成服务的初始化都在应用启动时完成,通过配置管理器加载配置并初始化各个服务。初始化流程确保了服务的线程安全和单例模式。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([应用启动]) --> LoadConfig["加载配置 (config.Load)"]
|
||||
LoadConfig --> InitEmail["初始化邮件服务 (email.Init)"]
|
||||
LoadConfig --> InitStorage["初始化存储服务 (storage.Init)"]
|
||||
LoadConfig --> InitRedis["初始化Redis服务 (redis.Init)"]
|
||||
InitEmail --> CheckEmail["检查邮件服务是否启用"]
|
||||
InitStorage --> TestStorage["测试存储连接"]
|
||||
InitRedis --> TestRedis["测试Redis连接"]
|
||||
CheckEmail --> End1([邮件服务就绪])
|
||||
TestStorage --> End2([存储服务就绪])
|
||||
TestRedis --> End3([Redis服务就绪])
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L109-L133)
|
||||
- [manager.go](file://pkg/email/manager.go#L20-L26)
|
||||
- [manager.go](file://pkg/storage/manager.go#L19-L26)
|
||||
- [manager.go](file://pkg/redis/manager.go#L21-L28)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L109-L133)
|
||||
- [manager.go](file://pkg/email/manager.go#L20-L26)
|
||||
- [manager.go](file://pkg/storage/manager.go#L19-L26)
|
||||
- [manager.go](file://pkg/redis/manager.go#L21-L28)
|
||||
|
||||
## 故障排除与性能调优
|
||||
|
||||
### 邮件服务
|
||||
- **常见问题**:SMTP连接失败、认证失败、邮件发送超时。
|
||||
- **解决方案**:检查SMTP主机和端口配置,确保SSL/TLS设置正确,验证用户名和密码。
|
||||
- **性能调优**:使用连接池减少连接开销,批量发送邮件以减少网络延迟。
|
||||
|
||||
### 对象存储
|
||||
- **常见问题**:预签名URL无效、上传失败、存储桶不存在。
|
||||
- **解决方案**:检查存储桶名称和权限,确保预签名URL的过期时间合理,验证访问密钥和密钥。
|
||||
- **性能调优**:使用分块上传大文件,启用CDN加速文件访问。
|
||||
|
||||
### 缓存系统
|
||||
- **常见问题**:Redis连接失败、内存不足、键过期。
|
||||
- **解决方案**:检查Redis主机和端口配置,优化键的过期时间,监控内存使用情况。
|
||||
- **性能调优**:使用管道减少网络往返,合理设置连接池大小,定期清理过期键。
|
||||
|
||||
**Section sources**
|
||||
- [email.go](file://pkg/email/email.go#L66-L86)
|
||||
- [minio.go](file://pkg/storage/minio.go#L33-L42)
|
||||
- [redis.go](file://pkg/redis/redis.go#L34-L40)
|
||||
|
||||
## 总结
|
||||
CarrotSkin项目通过集成邮件服务、对象存储和缓存系统,实现了高效、可靠的用户验证和文件存储功能。这些集成通过统一的配置管理和初始化流程,确保了服务的稳定性和可维护性。开发者可以根据本文档的指导,轻松配置和使用这些外部服务,并通过故障排除和性能调优技巧,进一步提升系统的性能和可靠性。
|
||||
@@ -1,345 +0,0 @@
|
||||
# 对象存储集成
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [minio.go](file://pkg/storage/minio.go)
|
||||
- [manager.go](file://pkg/storage/manager.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [upload_service.go](file://internal/service/upload_service.go)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go)
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向CarrotSkin后端的对象存储集成,聚焦于如何通过MinIO客户端与S3兼容存储系统交互。文档围绕以下目标展开:
|
||||
- 解析minio.go中初始化客户端的流程,包括访问密钥、端点配置和TLS设置
|
||||
- 说明manager.go中Upload、Download、GeneratePresignedURL等关键方法的实现细节与调用方式
|
||||
- 提供皮肤/披风文件上传、私有资源临时链接生成等典型用例的代码示例路径
|
||||
- 解释分片上传、断点续传等高级功能的支持情况
|
||||
- 为运维人员提供性能调优建议(并发控制、连接复用)和故障排查指南(签名错误、网络超时)
|
||||
|
||||
## 项目结构
|
||||
对象存储相关代码集中在pkg/storage目录,配置在pkg/config,业务侧在internal/service与internal/handler中调用。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置层"
|
||||
CFG["pkg/config/config.go<br/>RustFSConfig"]
|
||||
end
|
||||
subgraph "存储客户端"
|
||||
MINIO["pkg/storage/minio.go<br/>StorageClient<br/>NewStorage/GeneratePresignedURL/GeneratePresignedPostURL"]
|
||||
MGR["pkg/storage/manager.go<br/>Init/GetClient/MustGetClient"]
|
||||
end
|
||||
subgraph "业务服务"
|
||||
SVC_UPLOAD["internal/service/upload_service.go<br/>GenerateAvatarUploadURL/GenerateTextureUploadURL"]
|
||||
SVC_TEX["internal/service/texture_service.go<br/>CreateTexture/RecordTextureDownload 等"]
|
||||
end
|
||||
subgraph "接口层"
|
||||
HANDLER_TEX["internal/handler/texture_handler.go<br/>GenerateTextureUploadURL 接口"]
|
||||
end
|
||||
CFG --> MINIO
|
||||
MINIO --> MGR
|
||||
MGR --> HANDLER_TEX
|
||||
HANDLER_TEX --> SVC_UPLOAD
|
||||
SVC_UPLOAD --> MINIO
|
||||
SVC_TEX --> MINIO
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L120)
|
||||
- [manager.go](file://pkg/storage/manager.go#L1-L49)
|
||||
- [config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
|
||||
章节来源
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L120)
|
||||
- [manager.go](file://pkg/storage/manager.go#L1-L49)
|
||||
- [config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
|
||||
## 核心组件
|
||||
- StorageClient:封装minio-go客户端,提供桶名解析、预签名URL生成等能力
|
||||
- manager:提供全局单例的存储客户端初始化与获取
|
||||
- RustFSConfig:承载S3兼容存储的端点、凭据与桶映射
|
||||
- upload_service:面向业务的上传URL生成工具,按头像与材质类型分别组织对象路径
|
||||
- texture_handler:对外暴露生成上传URL的HTTP接口
|
||||
- texture_service:材质实体的增删改查与下载计数等业务逻辑
|
||||
|
||||
章节来源
|
||||
- [minio.go](file://pkg/storage/minio.go#L14-L120)
|
||||
- [manager.go](file://pkg/storage/manager.go#L9-L44)
|
||||
- [config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L13-L57)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
|
||||
## 架构总览
|
||||
下图展示从接口到存储的调用链路与职责划分。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "TextureHandler"
|
||||
participant S as "UploadService"
|
||||
participant M as "StorageManager"
|
||||
participant SC as "StorageClient"
|
||||
participant O as "对象存储(兼容S3)"
|
||||
C->>H : "POST /api/v1/texture/upload-url"
|
||||
H->>M : "MustGetClient()"
|
||||
M-->>H : "*StorageClient"
|
||||
H->>S : "GenerateTextureUploadURL(ctx, client, cfg, userID, fileName, type)"
|
||||
S->>SC : "GetBucket(\"textures\")"
|
||||
S->>SC : "GeneratePresignedPostURL(bucket, objectName, limits, expires, useSSL, endpoint)"
|
||||
SC->>O : "PresignedPostPolicy(policy)"
|
||||
O-->>SC : "postURL + formData"
|
||||
SC-->>S : "PresignedPostPolicyResult"
|
||||
S-->>H : "PresignedPostPolicyResult"
|
||||
H-->>C : "返回postURL、formData、文件最终URL、过期秒数"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- [manager.go](file://pkg/storage/manager.go#L30-L44)
|
||||
- [minio.go](file://pkg/storage/minio.go#L57-L120)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### StorageClient与初始化流程(minio.go)
|
||||
- 客户端初始化
|
||||
- 通过端点、凭据与TLS标志创建minio-go客户端
|
||||
- 若提供了访问密钥与密钥,则进行连接测试(带超时)
|
||||
- 绑定桶映射,便于按“类型”解析真实桶名
|
||||
- 关键方法
|
||||
- GetClient:返回底层minio.Client
|
||||
- GetBucket:按名称解析桶名,不存在时报错
|
||||
- GeneratePresignedURL:生成预签名PUT URL(支持上传)
|
||||
- GeneratePresignedPostURL:生成预签名POST策略URL(支持表单直传),并构造最终访问URL
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class StorageClient {
|
||||
-client : "minio.Client"
|
||||
-buckets : "map[string]string"
|
||||
+GetClient() "*minio.Client"
|
||||
+GetBucket(name) "(string, error)"
|
||||
+GeneratePresignedURL(ctx, bucketName, objectName, expires) "(string, error)"
|
||||
+GeneratePresignedPostURL(ctx, bucketName, objectName, minSize, maxSize, expires, useSSL, endpoint) "(*PresignedPostPolicyResult, error)"
|
||||
}
|
||||
class PresignedPostPolicyResult {
|
||||
+PostURL : "string"
|
||||
+FormData : "map[string]string"
|
||||
+FileURL : "string"
|
||||
}
|
||||
StorageClient --> PresignedPostPolicyResult : "返回"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [minio.go](file://pkg/storage/minio.go#L14-L120)
|
||||
|
||||
章节来源
|
||||
- [minio.go](file://pkg/storage/minio.go#L20-L49)
|
||||
- [minio.go](file://pkg/storage/minio.go#L52-L64)
|
||||
- [minio.go](file://pkg/storage/minio.go#L66-L73)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L120)
|
||||
|
||||
### 存储客户端管理器(manager.go)
|
||||
- Init(cfg):线程安全初始化,仅执行一次
|
||||
- GetClient():获取全局实例,未初始化时报错
|
||||
- MustGetClient():获取全局实例,未初始化时panic
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["调用 Init(cfg)"]) --> Once["sync.Once 保证只执行一次"]
|
||||
Once --> NewStorage["NewStorage(cfg) 创建 StorageClient"]
|
||||
NewStorage --> Bind["绑定 clientInstance"]
|
||||
Bind --> Done(["初始化完成"])
|
||||
GetClient["GetClient()"] --> Check{"clientInstance 是否存在?"}
|
||||
Check --> |否| Err["返回错误:未初始化"]
|
||||
Check --> |是| Return["返回 clientInstance"]
|
||||
MustGetClient["MustGetClient()"] --> GetClient
|
||||
GetClient --> Panic{"err 是否非空?"}
|
||||
Panic --> |是| PanicCall["panic(err)"]
|
||||
Panic --> |否| Return
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [manager.go](file://pkg/storage/manager.go#L9-L44)
|
||||
|
||||
章节来源
|
||||
- [manager.go](file://pkg/storage/manager.go#L18-L27)
|
||||
- [manager.go](file://pkg/storage/manager.go#L29-L35)
|
||||
- [manager.go](file://pkg/storage/manager.go#L37-L44)
|
||||
|
||||
### 配置结构(config.go)
|
||||
- RustFSConfig包含端点、访问密钥、密钥、TLS开关与桶映射
|
||||
- 环境变量映射与默认值设置,支持通过环境变量覆盖
|
||||
|
||||
章节来源
|
||||
- [config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [config.go](file://pkg/config/config.go#L190-L236)
|
||||
- [config.go](file://pkg/config/config.go#L238-L305)
|
||||
|
||||
### 上传服务与典型用例(upload_service.go)
|
||||
- 文件类型与上传配置
|
||||
- FileTypeAvatar/FileTypeTexture两类
|
||||
- 各自的允许扩展名、最小/最大尺寸、过期时间
|
||||
- 生成上传URL
|
||||
- GenerateAvatarUploadURL:按用户ID与时间戳生成对象路径,调用StorageClient.GeneratePresignedPostURL
|
||||
- GenerateTextureUploadURL:支持SKIN/CAPE两种材质类型,生成对应路径,调用StorageClient.GeneratePresignedPostURL
|
||||
- 下载与公开资源
|
||||
- 当前代码未直接暴露Download方法;公开资源可通过最终访问URL直接访问
|
||||
- 私有资源可通过GeneratePresignedURL生成临时下载链接
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Svc as "UploadService"
|
||||
participant SC as "StorageClient"
|
||||
participant O as "对象存储"
|
||||
Svc->>Svc : "ValidateFileName(fileName, type)"
|
||||
Svc->>SC : "GetBucket(\"avatars\" 或 \"textures\")"
|
||||
Svc->>SC : "GeneratePresignedPostURL(bucket, objectName, minSize, maxSize, expires, useSSL, endpoint)"
|
||||
SC->>O : "PresignedPostPolicy(policy)"
|
||||
O-->>SC : "postURL + formData"
|
||||
SC-->>Svc : "PresignedPostPolicyResult"
|
||||
Svc-->>Svc : "返回结果"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L120)
|
||||
|
||||
章节来源
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L13-L57)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L78-L115)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
|
||||
### 接口层调用(texture_handler.go)
|
||||
- GenerateTextureUploadURL接口:接收请求体,调用UploadService生成预签名POST URL,返回postURL、formData与最终文件URL
|
||||
- CreateTexture接口:文件上传完成后,创建材质记录到数据库
|
||||
|
||||
章节来源
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
|
||||
|
||||
### 材质服务(texture_service.go)
|
||||
- CreateTexture:校验用户存在、去重校验哈希、转换材质类型、创建记录
|
||||
- RecordTextureDownload:增加下载计数并记录日志
|
||||
- 其他查询与权限控制方法
|
||||
|
||||
章节来源
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L162-L187)
|
||||
|
||||
## 依赖关系分析
|
||||
- 配置到客户端:RustFSConfig -> NewStorage -> StorageClient
|
||||
- 客户端到管理器:StorageClient -> Manager(Init/GetClient/MustGetClient)
|
||||
- 业务到客户端:UploadService/TextureService -> StorageClient
|
||||
- 接口到业务:TextureHandler -> UploadService -> StorageClient
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
CFG["RustFSConfig"] --> NS["NewStorage(cfg)"]
|
||||
NS --> SC["StorageClient"]
|
||||
SC --> M["Manager.Init/GetClient/MustGetClient"]
|
||||
M --> H["TextureHandler"]
|
||||
H --> US["UploadService"]
|
||||
US --> SC
|
||||
TS["TextureService"] --> SC
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [minio.go](file://pkg/storage/minio.go#L20-L49)
|
||||
- [manager.go](file://pkg/storage/manager.go#L18-L44)
|
||||
- [upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
|
||||
## 性能考虑
|
||||
- 并发控制
|
||||
- 上传直传采用预签名POST策略,客户端直接向对象存储发起请求,避免服务端转发带来的CPU与内存压力
|
||||
- 服务端仅负责生成策略与返回表单数据,适合高并发场景
|
||||
- 连接复用
|
||||
- minio-go客户端内部维护连接池与HTTP复用;建议在生产环境中保持长连接,避免频繁重建
|
||||
- 超时与重试
|
||||
- 初始化阶段对ListBuckets设置了超时,防止阻塞启动
|
||||
- 业务侧建议在调用方为上传/下载操作设置合理超时与指数退避重试
|
||||
- 缓存与预热
|
||||
- 对频繁使用的桶名解析与策略生成可做缓存(需注意策略过期时间)
|
||||
- 资源限制
|
||||
- 通过上传配置限制文件大小与扩展名,降低存储与带宽压力
|
||||
|
||||
[本节为通用指导,无需特定文件引用]
|
||||
|
||||
## 故障排查指南
|
||||
- 签名错误
|
||||
- 现象:表单上传返回签名错误
|
||||
- 排查要点:确认formData中多余字段被移除;确保file字段位于表单末尾;检查endpoint与useSSL一致性
|
||||
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L82-L120)
|
||||
- 网络超时
|
||||
- 现象:初始化或ListBuckets超时
|
||||
- 排查要点:检查endpoint连通性、防火墙、TLS证书;适当增大超时时间
|
||||
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L33-L42)
|
||||
- 凭据错误
|
||||
- 现象:连接测试失败或策略生成失败
|
||||
- 排查要点:核对AccessKey/SecretKey;确认桶映射正确;检查对象存储端策略与权限
|
||||
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L20-L49)
|
||||
- 桶不存在
|
||||
- 现象:GetBucket返回错误
|
||||
- 排查要点:确认RustFSConfig.Buckets中包含所需桶名;核对环境变量覆盖逻辑
|
||||
- 参考实现位置:[minio.go](file://pkg/storage/minio.go#L57-L64),[config.go](file://pkg/config/config.go#L238-L253)
|
||||
- 接口调用失败
|
||||
- 现象:接口返回400/500
|
||||
- 排查要点:检查鉴权中间件、请求体绑定、日志输出;确认MustGetClient/MustGetRustFSConfig已调用
|
||||
- 参考实现位置:[texture_handler.go](file://internal/handler/texture_handler.go#L18-L83),[manager.go](file://pkg/storage/manager.go#L37-L44),[config.go](file://pkg/config/config.go#L190-L236)
|
||||
|
||||
章节来源
|
||||
- [minio.go](file://pkg/storage/minio.go#L20-L49)
|
||||
- [minio.go](file://pkg/storage/minio.go#L57-L64)
|
||||
- [minio.go](file://pkg/storage/minio.go#L82-L120)
|
||||
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- [manager.go](file://pkg/storage/manager.go#L37-L44)
|
||||
- [config.go](file://pkg/config/config.go#L238-L253)
|
||||
|
||||
## 结论
|
||||
- CarrotSkin通过StorageClient与minio-go实现了对S3兼容存储的完整封装,重点支持预签名POST直传与PUT上传
|
||||
- 上传流程清晰:接口层生成策略,前端直传对象存储,服务端仅负责策略与元数据登记
|
||||
- 分片上传与断点续传当前未在代码中直接体现;如需支持,可在客户端侧采用分片上传策略,并在服务端补充断点续传与合并逻辑
|
||||
- 建议结合业务需求完善下载与公开/私有资源的访问策略,并持续优化并发与连接复用
|
||||
|
||||
[本节为总结性内容,无需特定文件引用]
|
||||
|
||||
## 附录
|
||||
|
||||
### 典型用例示例(代码示例路径)
|
||||
- 皮肤/披风文件上传
|
||||
- 生成预签名POST URL:[upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- 接口调用入口:[texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
|
||||
- 私有资源临时链接生成(PUT)
|
||||
- 生成预签名PUT URL:[minio.go](file://pkg/storage/minio.go#L66-L73)
|
||||
- 业务侧调用:[upload_service.go](file://internal/service/upload_service.go#L78-L115)
|
||||
|
||||
### 高级功能支持现状
|
||||
- 分片上传/断点续传
|
||||
- 当前未在代码中直接实现;如需支持,可在客户端侧引入分片上传策略,并在服务端补充断点续传与合并逻辑
|
||||
- 下载
|
||||
- 当前未直接暴露Download方法;可通过最终访问URL或GeneratePresignedURL生成临时下载链接
|
||||
|
||||
[本节为概念性说明,无需特定文件引用]
|
||||
@@ -1,361 +0,0 @@
|
||||
# 邮件服务集成
|
||||
|
||||
<cite>
|
||||
**本文引用的文件列表**
|
||||
- [email.go](file://pkg/email/email.go)
|
||||
- [manager.go](file://pkg/email/manager.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [verification_service.go](file://internal/service/verification_service.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [redis.go](file://pkg/redis/redis.go)
|
||||
- [manager.go](file://pkg/redis/manager.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能与可靠性](#性能与可靠性)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向CarrotSkin项目的开发者与运维人员,系统性介绍基于SMTP的邮件服务实现与集成方式。重点覆盖以下内容:
|
||||
- 邮件服务核心结构体与方法(如Service、SendVerificationCode等)的实现逻辑
|
||||
- SMTP服务器参数配置(主机、端口、认证信息)与客户端初始化流程
|
||||
- 注册验证、密码重置、更换邮箱等场景的邮件发送流程
|
||||
- 与用户认证流程的集成方式(验证码生成、有效期管理、频率限制与安全考虑)
|
||||
- 初学者本地测试配置建议(如使用MailHog)
|
||||
- 高级用户可扩展的方向(错误重试机制、连接池优化、监控指标)
|
||||
|
||||
## 项目结构
|
||||
邮件服务位于独立的pkg模块中,并与配置、Redis缓存、业务服务层以及HTTP处理器协同工作。整体组织如下:
|
||||
- 配置层:集中定义EmailConfig并通过环境变量加载
|
||||
- 邮件服务层:封装SMTP发送、主题与HTML正文构建
|
||||
- 缓存层:使用Redis存储验证码与频率限制
|
||||
- 业务服务层:生成验证码、校验验证码、根据类型路由到具体邮件方法
|
||||
- HTTP处理器:对外暴露发送验证码接口,调用业务服务层
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置层"
|
||||
CFG["EmailConfig<br/>环境变量绑定"]
|
||||
end
|
||||
subgraph "邮件服务层"
|
||||
SVC["Service<br/>send()/Send* 方法"]
|
||||
MGR["manager.go<br/>Init/GetService/MustGetService"]
|
||||
end
|
||||
subgraph "缓存层"
|
||||
RCL["Redis Client<br/>Set/Get/Del/Exists"]
|
||||
end
|
||||
subgraph "业务服务层"
|
||||
VSRV["verification_service.go<br/>SendVerificationCode/VerifyCode"]
|
||||
end
|
||||
subgraph "HTTP处理器"
|
||||
AH["auth_handler.go<br/>SendVerificationCode 接口"]
|
||||
end
|
||||
CFG --> MGR
|
||||
MGR --> SVC
|
||||
SVC --> RCL
|
||||
AH --> VSRV
|
||||
VSRV --> SVC
|
||||
VSRV --> RCL
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [email.go](file://pkg/email/email.go#L1-L163)
|
||||
- [manager.go](file://pkg/email/manager.go#L1-L48)
|
||||
- [config.go](file://pkg/config/config.go#L98-L107)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
章节来源
|
||||
- [email.go](file://pkg/email/email.go#L1-L163)
|
||||
- [manager.go](file://pkg/email/manager.go#L1-L48)
|
||||
- [config.go](file://pkg/config/config.go#L98-L107)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
## 核心组件
|
||||
- Service(邮件服务)
|
||||
- 职责:封装SMTP发送、主题与HTML正文构建、TLS/STARTTLS选择、日志记录
|
||||
- 关键方法:SendVerificationCode、SendResetPassword、SendEmailVerification、SendChangeEmail、send
|
||||
- manager(邮件服务管理器)
|
||||
- 职责:全局单例初始化、线程安全获取实例、未初始化保护
|
||||
- 关键方法:Init、GetService、MustGetService
|
||||
- 配置(EmailConfig)
|
||||
- 字段:Enabled、SMTPHost、SMTPPort、Username、Password、FromName
|
||||
- 来源:环境变量绑定与默认值设置
|
||||
- 验证码服务(verification_service)
|
||||
- 职责:生成6位数字验证码、存储到Redis、设置频率限制、按类型路由到具体邮件方法
|
||||
- Redis客户端(redis)
|
||||
- 职责:连接管理、基础KV与集合操作、连接健康检查
|
||||
- HTTP处理器(auth_handler)
|
||||
- 职责:对外暴露发送验证码接口,调用业务服务层
|
||||
|
||||
章节来源
|
||||
- [email.go](file://pkg/email/email.go#L1-L163)
|
||||
- [manager.go](file://pkg/email/manager.go#L1-L48)
|
||||
- [config.go](file://pkg/config/config.go#L98-L107)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
## 架构总览
|
||||
下图展示从HTTP请求到邮件发送的完整链路,包括验证码生成、存储、频率限制与邮件发送。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "auth_handler.SendVerificationCode"
|
||||
participant VS as "verification_service.SendVerificationCode"
|
||||
participant RS as "Redis Client"
|
||||
participant ES as "email.Service"
|
||||
participant SMTP as "SMTP服务器"
|
||||
C->>H : "POST /api/v1/auth/send-code"
|
||||
H->>VS : "调用发送验证码(类型, 邮箱)"
|
||||
VS->>RS : "检查频率限制(键 : rate_limit)"
|
||||
RS-->>VS : "存在/不存在"
|
||||
VS->>VS : "生成6位数字验证码"
|
||||
VS->>RS : "写入验证码(键 : code)"
|
||||
VS->>RS : "设置频率限制(键 : rate_limit)"
|
||||
VS->>ES : "根据类型调用具体发送方法"
|
||||
ES->>SMTP : "SMTP发送(465隐式TLS/587 STARTTLS)"
|
||||
SMTP-->>ES : "结果"
|
||||
ES-->>VS : "结果"
|
||||
VS-->>H : "结果"
|
||||
H-->>C : "发送成功响应"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [email.go](file://pkg/email/email.go#L57-L105)
|
||||
- [redis.go](file://pkg/redis/redis.go#L60-L78)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### Service(邮件服务)类图
|
||||
Service负责SMTP发送、主题与HTML正文构建、TLS/STARTTLS选择与日志记录。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Service {
|
||||
-cfg : EmailConfig
|
||||
-logger : Logger
|
||||
+SendVerificationCode(to, code, purpose) error
|
||||
+SendResetPassword(to, code) error
|
||||
+SendEmailVerification(to, code) error
|
||||
+SendChangeEmail(to, code) error
|
||||
-send(to, subject, body) error
|
||||
-getSubject(purpose) string
|
||||
-getBody(code, purpose) string
|
||||
}
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [email.go](file://pkg/email/email.go#L1-L163)
|
||||
|
||||
章节来源
|
||||
- [email.go](file://pkg/email/email.go#L1-L163)
|
||||
|
||||
### 发送流程与TLS策略
|
||||
- 主题与正文
|
||||
- 主题根据用途动态选择(邮箱验证、重置密码、更换邮箱)
|
||||
- 正文为HTML模板,包含验证码展示与有效期提示
|
||||
- SMTP连接策略
|
||||
- 465端口:使用隐式TLS(SendWithTLS)
|
||||
- 587端口:使用STARTTLS(Send)
|
||||
- 认证方式
|
||||
- 使用PlainAuth进行SMTP认证
|
||||
- 错误处理
|
||||
- 失败时记录详细上下文(收件人、主题、SMTP主机与端口)
|
||||
- 成功时记录成功日志
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入 send 方法"]) --> Build["构建邮件对象<br/>From/To/Subject/HTML/Headers"]
|
||||
Build --> Auth["构造SMTP认证信息"]
|
||||
Auth --> Addr["拼接地址 host:port"]
|
||||
Addr --> PortCheck{"端口为465?"}
|
||||
PortCheck --> |是| TLS["配置TLS(隐式)<br/>SendWithTLS"]
|
||||
PortCheck --> |否| StartTLS["Send(显式STARTTLS)"]
|
||||
TLS --> Result{"发送成功?"}
|
||||
StartTLS --> Result
|
||||
Result --> |否| LogErr["记录错误日志并返回错误"]
|
||||
Result --> |是| LogOK["记录成功日志并返回nil"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [email.go](file://pkg/email/email.go#L57-L105)
|
||||
|
||||
章节来源
|
||||
- [email.go](file://pkg/email/email.go#L57-L105)
|
||||
|
||||
### 验证码生成与存储(verification_service)
|
||||
- 验证码生成
|
||||
- 6位纯数字随机验证码
|
||||
- 存储与有效期
|
||||
- Redis键命名规范:verification:code:{type}:{email},有效期10分钟
|
||||
- 频率限制
|
||||
- Redis键:verification:rate_limit:{type}:{email},限制1分钟内仅允许一次
|
||||
- 类型路由
|
||||
- register -> SendEmailVerification
|
||||
- reset_password -> SendResetPassword
|
||||
- change_email -> SendChangeEmail
|
||||
- 其他 -> SendVerificationCode
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Enter(["进入 SendVerificationCode"]) --> RL["检查频率限制键是否存在"]
|
||||
RL --> |存在| RateErr["返回发送过于频繁错误"]
|
||||
RL --> |不存在| Gen["生成6位验证码"]
|
||||
Gen --> Store["写入Redis: code键 + 过期10分钟"]
|
||||
Store --> Limit["写入Redis: rate_limit键 + 过期1分钟"]
|
||||
Limit --> Route{"根据类型路由"}
|
||||
Route --> |register| SendEV["SendEmailVerification"]
|
||||
Route --> |reset_password| SendRP["SendResetPassword"]
|
||||
Route --> |change_email| SendCE["SendChangeEmail"]
|
||||
Route --> |其他| SendVC["SendVerificationCode"]
|
||||
SendEV --> Done(["返回成功"])
|
||||
SendRP --> Done
|
||||
SendCE --> Done
|
||||
SendVC --> Done
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
|
||||
章节来源
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||||
|
||||
### HTTP处理器集成(auth_handler)
|
||||
- 发送验证码接口
|
||||
- 解析请求、调用业务服务层、记录日志、返回响应
|
||||
- 注册流程中的验证码校验
|
||||
- 在注册接口中调用VerifyCode校验验证码,失败即拒绝注册
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L79-L98)
|
||||
|
||||
## 依赖关系分析
|
||||
- 组件耦合
|
||||
- Service依赖EmailConfig与Zap日志
|
||||
- verification_service依赖Redis Client与email.Service
|
||||
- auth_handler依赖verification_service与日志、Redis
|
||||
- 外部依赖
|
||||
- SMTP客户端库用于发送邮件
|
||||
- Redis客户端库用于KV与限流
|
||||
- 可能的循环依赖
|
||||
- 当前结构清晰,未见循环依赖迹象
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
CFG["EmailConfig"] --> MGR["manager.Init/GetService"]
|
||||
MGR --> SVC["Service"]
|
||||
SVC --> SMTP["SMTP服务器"]
|
||||
VS["verification_service"] --> SVC
|
||||
VS --> RCL["Redis Client"]
|
||||
AH["auth_handler"] --> VS
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [email.go](file://pkg/email/email.go#L1-L163)
|
||||
- [manager.go](file://pkg/email/manager.go#L1-L48)
|
||||
- [config.go](file://pkg/config/config.go#L98-L107)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
章节来源
|
||||
- [email.go](file://pkg/email/email.go#L1-L163)
|
||||
- [manager.go](file://pkg/email/manager.go#L1-L48)
|
||||
- [config.go](file://pkg/config/config.go#L98-L107)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L1-L119)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
## 性能与可靠性
|
||||
- 连接池与并发
|
||||
- Redis客户端内置连接池(PoolSize),建议结合业务规模调整
|
||||
- 邮件发送为短连接,建议避免在同一请求中频繁发送
|
||||
- 错误重试
|
||||
- 当前实现未内置自动重试;可在业务层增加指数退避重试策略
|
||||
- 监控指标
|
||||
- 建议采集:邮件发送成功率、耗时、失败原因分布、Redis读写延迟
|
||||
- 安全与合规
|
||||
- 生产环境TLS校验建议开启(InsecureSkipVerify=false)
|
||||
- 密码等敏感信息仅在内存中传输,避免落盘
|
||||
|
||||
[本节为通用建议,无需列出章节来源]
|
||||
|
||||
## 故障排查指南
|
||||
- 常见问题定位
|
||||
- 邮件服务未启用:检查EmailConfig.Enabled与环境变量
|
||||
- SMTP认证失败:核对Username/Password与SMTPHost/SMTPPort
|
||||
- TLS握手失败:确认端口与TLS策略匹配(465隐式TLS vs 587 STARTTLS)
|
||||
- Redis连接失败:检查Redis配置与网络连通性
|
||||
- 日志与告警
|
||||
- 发送失败会记录详细上下文,优先查看日志中的错误堆栈
|
||||
- 建议在网关或进程外添加告警规则(如连续失败阈值)
|
||||
- 本地测试
|
||||
- 使用MailHog作为本地SMTP代理,便于调试与演示
|
||||
|
||||
章节来源
|
||||
- [email.go](file://pkg/email/email.go#L57-L105)
|
||||
- [config.go](file://pkg/config/config.go#L229-L236)
|
||||
- [redis.go](file://pkg/redis/redis.go#L22-L52)
|
||||
|
||||
## 结论
|
||||
CarrotSkin的邮件服务以简洁、可测试的方式实现了基于SMTP的验证码邮件发送,配合Redis完成验证码生成、存储与频率控制。通过HTTP处理器与业务服务层的清晰分层,系统具备良好的可维护性与扩展性。建议在生产环境中完善错误重试、连接池优化与监控指标,以进一步提升稳定性与可观测性。
|
||||
|
||||
[本节为总结性内容,无需列出章节来源]
|
||||
|
||||
## 附录
|
||||
|
||||
### SMTP配置与初始化
|
||||
- 配置项(EmailConfig)
|
||||
- Enabled:是否启用邮件功能
|
||||
- SMTPHost:SMTP服务器主机
|
||||
- SMTPPort:SMTP服务器端口(常用465或587)
|
||||
- Username/Password:SMTP认证凭据
|
||||
- FromName:发件人显示名称
|
||||
- 环境变量绑定
|
||||
- EMAIL_ENABLED、EMAIL_SMTP_HOST、EMAIL_SMTP_PORT、EMAIL_USERNAME、EMAIL_PASSWORD、EMAIL_FROM_NAME
|
||||
- 初始化步骤
|
||||
- 通过manager.Init传入EmailConfig与Logger,随后通过MustGetService获取实例
|
||||
- 在应用启动阶段完成初始化,确保后续调用可用
|
||||
|
||||
章节来源
|
||||
- [config.go](file://pkg/config/config.go#L98-L107)
|
||||
- [config.go](file://pkg/config/config.go#L229-L236)
|
||||
- [manager.go](file://pkg/email/manager.go#L20-L43)
|
||||
|
||||
### 场景化使用路径
|
||||
- 注册验证
|
||||
- HTTP接口:auth_handler.SendVerificationCode
|
||||
- 业务流程:verification_service.SendVerificationCode -> email.Service.SendEmailVerification
|
||||
- 密码重置
|
||||
- 业务流程:verification_service.SendVerificationCode -> email.Service.SendResetPassword
|
||||
- 更换邮箱
|
||||
- 业务流程:verification_service.SendVerificationCode -> email.Service.SendChangeEmail
|
||||
|
||||
章节来源
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L149-L192)
|
||||
- [verification_service.go](file://internal/service/verification_service.go#L40-L118)
|
||||
- [email.go](file://pkg/email/email.go#L42-L55)
|
||||
|
||||
### 本地测试建议(MailHog)
|
||||
- 启动MailHog作为本地SMTP代理
|
||||
- 将EmailConfig配置指向MailHog的SMTP地址与端口
|
||||
- 通过auth_handler的发送验证码接口验证端到端流程
|
||||
- 在MailHog Web界面查看邮件内容与收件人信息
|
||||
|
||||
[本节为通用建议,无需列出章节来源]
|
||||
@@ -1,325 +0,0 @@
|
||||
# 快速开始
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [go.mod](file://go.mod)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
|
||||
- [scripts/dev.sh](file://scripts/dev.sh)
|
||||
- [run.sh](file://run.sh)
|
||||
- [run.bat](file://run.bat)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
- [configs/casbin/rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能与运行建议](#性能与运行建议)
|
||||
8. [故障排除指南](#故障排除指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本指南面向首次接触 CarrotSkin 后端项目的开发者,帮助你从零开始完成开发环境准备、配置与依赖安装,并通过提供的脚本快速启动服务。你将学会:
|
||||
- 准备 Go、数据库与对象存储等运行所需环境
|
||||
- 复制并配置环境变量与配置文件
|
||||
- 安装 Swagger 工具并生成 API 文档
|
||||
- 使用 dev.sh、run.sh 和 run.bat 脚本启动项目
|
||||
- 面向初学者的路径指引与面向资深用户的常见问题排查
|
||||
|
||||
## 项目结构
|
||||
CarrotSkin 后端采用模块化分层设计,主要目录与职责如下:
|
||||
- cmd/server:应用入口,负责初始化配置、数据库、中间件、路由与 HTTP 服务
|
||||
- pkg/*:通用能力封装,如配置、数据库、Redis、存储、日志、邮件等
|
||||
- internal/*:业务层实现,包括处理器、中间件、模型、仓库、服务与类型定义
|
||||
- scripts:开发与环境检查脚本
|
||||
- configs:静态配置与权限模型文件
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A["cmd/server/main.go<br/>应用入口"] --> B["pkg/config/*<br/>配置加载与管理"]
|
||||
A --> C["pkg/database/*<br/>数据库初始化与迁移"]
|
||||
A --> D["pkg/redis/*<br/>Redis连接"]
|
||||
A --> E["pkg/storage/*<br/>对象存储(RustFS)"]
|
||||
A --> F["pkg/email/*<br/>邮件服务"]
|
||||
A --> G["internal/handler/*<br/>路由与Swagger"]
|
||||
A --> H["internal/middleware/*<br/>中间件"]
|
||||
A --> I["internal/service/*<br/>业务服务"]
|
||||
A --> J["internal/repository/*<br/>数据访问层"]
|
||||
A --> K["internal/model/*<br/>数据模型"]
|
||||
L["scripts/*<br/>启动与检查脚本"] --> A
|
||||
M["configs/casbin/*<br/>权限模型"] --> I
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
|
||||
- [scripts/dev.sh](file://scripts/dev.sh#L1-L29)
|
||||
- [configs/casbin/rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
|
||||
|
||||
章节来源
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
|
||||
## 核心组件
|
||||
- 配置系统:支持从 .env 与环境变量加载,提供默认值与覆盖逻辑,便于本地与生产环境切换
|
||||
- 数据库:基于 GORM 的 PostgreSQL 连接与自动迁移
|
||||
- 对象存储:RustFS(S3 兼容),用于材质与头像等资源存储
|
||||
- 中间件:日志、恢复、CORS
|
||||
- Swagger:自动生成 API 文档并通过路由暴露
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L68)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
|
||||
|
||||
## 架构总览
|
||||
下图展示了应用启动时的关键流程:加载配置 → 初始化日志 → 初始化数据库并迁移 → 初始化 JWT/Redis/存储/邮件 → 注册路由 → 启动 HTTP 服务。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as "命令行"
|
||||
participant Main as "main.go"
|
||||
participant Cfg as "配置模块"
|
||||
participant Log as "日志模块"
|
||||
participant DB as "数据库模块"
|
||||
participant JWT as "JWT服务"
|
||||
participant Rds as "Redis"
|
||||
participant Store as "对象存储"
|
||||
participant Mail as "邮件服务"
|
||||
participant Router as "路由与中间件"
|
||||
participant HTTP as "HTTP服务器"
|
||||
CLI->>Main : "go run cmd/server/main.go"
|
||||
Main->>Cfg : "Init()"
|
||||
Cfg-->>Main : "配置加载完成"
|
||||
Main->>Log : "Init(日志配置)"
|
||||
Main->>DB : "Init(数据库配置)"
|
||||
DB-->>Main : "连接成功"
|
||||
Main->>DB : "AutoMigrate()"
|
||||
DB-->>Main : "迁移完成"
|
||||
Main->>JWT : "Init(JWT配置)"
|
||||
Main->>Rds : "Init(Redis配置)"
|
||||
Main->>Store : "Init(RustFS配置)"
|
||||
Store-->>Main : "连接成功/告警"
|
||||
Main->>Mail : "Init(邮件配置)"
|
||||
Main->>Router : "注册中间件与路由"
|
||||
Main->>HTTP : "ListenAndServe()"
|
||||
HTTP-->>CLI : "服务就绪"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L68)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 配置系统与环境变量
|
||||
- 配置来源优先级:.env 文件 → 环境变量 → 默认值
|
||||
- 支持的环境变量前缀:CARROTSKIN(例如 CARROTSKIN_DATABASE_HOST)
|
||||
- 关键配置项(部分):服务器端口、模式、读写超时;数据库主机、端口、用户名、密码、库名、SSL、时区、连接池;Redis 主机、端口、密码、库号、池大小;RustFS Endpoint、AccessKey、SecretKey、UseSSL、存储桶映射;JWT Secret、过期小时;日志级别、格式、输出、轮转参数;上传最大尺寸、类型限制;邮件 SMTP Host/Port/用户名/密码/发送者名称
|
||||
- 特殊覆盖逻辑:可通过独立环境变量覆盖连接池、Redis 池大小、上传限制、邮件开关等
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L68)
|
||||
|
||||
### 数据库初始化与迁移
|
||||
- 初始化:根据配置建立 PostgreSQL 连接
|
||||
- 迁移:按顺序创建用户、档案、材质、认证、Yggdrasil、系统配置、审计日志、权限规则等表
|
||||
- 关闭:优雅关闭 SQL 连接
|
||||
|
||||
章节来源
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
|
||||
### 对象存储(RustFS/S3 兼容)
|
||||
- 初始化:根据配置连接到 S3 兼容存储
|
||||
- 行为:连接失败仅记录警告,不影响服务启动(部分功能受限)
|
||||
|
||||
章节来源
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
|
||||
|
||||
### Swagger 文档
|
||||
- 路由:/swagger/*any
|
||||
- 健康检查:/health
|
||||
- 文档生成:通过 swag 工具扫描注释生成
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
|
||||
|
||||
### 权限模型(Casbin)
|
||||
- RBAC 模型文件位于 configs/casbin/rbac_model.conf
|
||||
|
||||
章节来源
|
||||
- [configs/casbin/rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
|
||||
|
||||
## 依赖关系分析
|
||||
- 应用入口依赖配置、数据库、Redis、存储、邮件、日志与路由模块
|
||||
- 配置模块依赖 Viper 与 godotenv
|
||||
- 数据库模块依赖 GORM 与 PostgreSQL 驱动
|
||||
- 存储模块依赖 MinIO SDK
|
||||
- Swagger 依赖 Gin-Swagger 与 Swag 工具
|
||||
- Redis 依赖官方 Redis 客户端
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Main["cmd/server/main.go"] --> Cfg["pkg/config/*"]
|
||||
Main --> DB["pkg/database/*"]
|
||||
Main --> Rds["pkg/redis/*"]
|
||||
Main --> Stor["pkg/storage/*"]
|
||||
Main --> Mail["pkg/email/*"]
|
||||
Main --> Log["pkg/logger/*"]
|
||||
Main --> Hdl["internal/handler/*"]
|
||||
Cfg --> Viper["spf13/viper"]
|
||||
Cfg --> DotEnv["joho/godotenv"]
|
||||
DB --> GORM["gorm.io/gorm"]
|
||||
DB --> PGX["jackc/pgx"]
|
||||
Stor --> MinIO["minio/minio-go"]
|
||||
Hdl --> Gin["gin-gonic/gin"]
|
||||
Hdl --> Swagger["swaggo/gin-swagger"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
|
||||
章节来源
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
|
||||
## 性能与运行建议
|
||||
- 数据库连接池:通过环境变量覆盖最大空闲/活动连接数与连接生命周期,以适配不同负载
|
||||
- Redis 池大小:根据并发与延迟需求调整
|
||||
- 日志轮转:合理设置日志文件大小、保留份数与保留天数,避免磁盘压力
|
||||
- Swagger 生成:仅在开发阶段生成,生产环境可预生成并禁用动态生成
|
||||
|
||||
[本节为通用建议,无需特定文件来源]
|
||||
|
||||
## 故障排除指南
|
||||
|
||||
### 环境变量检查
|
||||
- 使用脚本检查 .env 是否存在、必需变量是否完整、敏感默认值是否已被修改
|
||||
- 建议在本地复制 .env.example 为 .env 并按需填写
|
||||
|
||||
章节来源
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
|
||||
|
||||
### Swagger 文档生成失败
|
||||
- 确认已安装 swag 工具(go install github.com/swaggo/swag/cmd/swag@latest)
|
||||
- 使用 run.sh 或 run.bat 自动安装与生成
|
||||
- 若使用 dev.sh,确保 swag 可用且目标入口文件路径正确
|
||||
|
||||
章节来源
|
||||
- [run.sh](file://run.sh#L1-L37)
|
||||
- [run.bat](file://run.bat#L1-L42)
|
||||
- [scripts/dev.sh](file://scripts/dev.sh#L1-L29)
|
||||
|
||||
### 数据库连接失败
|
||||
- 检查 DATABASE_HOST、DATABASE_PORT、DATABASE_NAME、DATABASE_USERNAME、DATABASE_PASSWORD
|
||||
- 确认数据库服务可达、SSL 模式与时区设置符合实际
|
||||
- 查看日志模块输出定位错误
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
|
||||
### 对象存储连接失败
|
||||
- 检查 RUSTFS_ENDPOINT、RUSTFS_ACCESS_KEY、RUSTFS_SECRET_KEY、RUSTFS_USE_SSL
|
||||
- 确认存储服务可用且网络连通
|
||||
- 若仅部分功能受限,可在日志中看到警告
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L1-L49)
|
||||
|
||||
### JWT 密钥安全
|
||||
- 确保 JWT_SECRET 长度足够且非默认值
|
||||
- 生产环境务必更换默认密钥
|
||||
|
||||
章节来源
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
|
||||
### 服务器启动与停止
|
||||
- 正常启动:使用 run.sh 或 run.bat,或直接 go run cmd/server/main.go
|
||||
- 停止服务:在终端按 Ctrl+C 触发优雅关闭
|
||||
|
||||
章节来源
|
||||
- [run.sh](file://run.sh#L1-L37)
|
||||
- [run.bat](file://run.bat#L1-L42)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
|
||||
## 结论
|
||||
通过本指南,你可以快速完成 CarrotSkin 后端的环境准备与项目启动。建议在本地先用 run.sh 或 run.bat 完成一键启动,随后使用 scripts/check-env.sh 校验环境变量,最后在开发过程中使用 dev.sh 进行迭代调试。遇到问题时,优先检查环境变量、数据库与对象存储连通性,并确认 swag 工具可用。
|
||||
|
||||
[本节为总结,无需特定文件来源]
|
||||
|
||||
## 附录
|
||||
|
||||
### 开发环境先决条件
|
||||
- Go 版本:项目要求 Go 1.23;工具链为 1.24.2
|
||||
- 数据库:PostgreSQL(驱动与连接参数见配置)
|
||||
- 对象存储:S3 兼容服务(RustFS Endpoint、AccessKey、SecretKey)
|
||||
- 其他:Redis(可选)、邮件 SMTP(可选)
|
||||
|
||||
章节来源
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
|
||||
### 配置与环境变量清单
|
||||
- 服务器:SERVER_PORT、SERVER_MODE、SERVER_READ_TIMEOUT、SERVER_WRITE_TIMEOUT
|
||||
- 数据库:DATABASE_DRIVER、DATABASE_HOST、DATABASE_PORT、DATABASE_USERNAME、DATABASE_PASSWORD、DATABASE_NAME、DATABASE_SSL_MODE、DATABASE_TIMEZONE、DATABASE_MAX_IDLE_CONNS、DATABASE_MAX_OPEN_CONNS、DATABASE_CONN_MAX_LIFETIME
|
||||
- Redis:REDIS_HOST、REDIS_PORT、REDIS_PASSWORD、REDIS_DATABASE、REDIS_POOL_SIZE
|
||||
- 对象存储:RUSTFS_ENDPOINT、RUSTFS_ACCESS_KEY、RUSTFS_SECRET_KEY、RUSTFS_USE_SSL、RUSTFS_BUCKET_TEXTURES、RUSTFS_BUCKET_AVATARS
|
||||
- JWT:JWT_SECRET、JWT_EXPIRE_HOURS
|
||||
- 日志:LOG_LEVEL、LOG_FORMAT、LOG_OUTPUT、LOG_MAX_SIZE、LOG_MAX_BACKUPS、LOG_MAX_AGE、LOG_COMPRESS
|
||||
- 上传:UPLOAD_MAX_SIZE、UPLOAD_TEXTURE_MAX_SIZE、UPLOAD_AVATAR_MAX_SIZE、UPLOAD_ALLOWED_TYPES
|
||||
- 邮件:EMAIL_ENABLED、EMAIL_SMTP_HOST、EMAIL_SMTP_PORT、EMAIL_USERNAME、EMAIL_PASSWORD、EMAIL_FROM_NAME
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
|
||||
### 启动脚本与命令说明
|
||||
- dev.sh(开发环境)
|
||||
- 自动复制配置示例为正式配置
|
||||
- 执行 go mod tidy
|
||||
- 检测并生成 Swagger 文档
|
||||
- 启动应用
|
||||
- run.sh(一次性安装并启动)
|
||||
- 自动安装 swag
|
||||
- 生成 Swagger 文档
|
||||
- 启动应用并打印访问地址
|
||||
- run.bat(Windows)
|
||||
- 自动安装 swag
|
||||
- 生成 Swagger 文档
|
||||
- 启动应用并打印访问地址
|
||||
|
||||
章节来源
|
||||
- [scripts/dev.sh](file://scripts/dev.sh#L1-L29)
|
||||
- [run.sh](file://run.sh#L1-L37)
|
||||
- [run.bat](file://run.bat#L1-L42)
|
||||
|
||||
### Swagger 文档访问
|
||||
- 服务地址:http://localhost:8080
|
||||
- Swagger 文档:http://localhost:8080/swagger/index.html
|
||||
- 健康检查:http://localhost:8080/health
|
||||
|
||||
章节来源
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go#L1-L63)
|
||||
- [run.sh](file://run.sh#L1-L37)
|
||||
- [run.bat](file://run.bat#L1-L42)
|
||||
@@ -1,306 +0,0 @@
|
||||
# 技术栈与依赖
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
- [logger.go](file://pkg/logger/logger.go#L1-L69)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
- [cors.go](file://internal/middleware/cors.go#L1-L23)
|
||||
- [swagger.go](file://internal/handler/swagger.go#L1-L63)
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
|
||||
- [run.sh](file://run.sh#L1-L37)
|
||||
- [start.sh](file://start.sh#L1-L41)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [技术选型概览](#技术选型概览)
|
||||
2. [核心框架与库详解](#核心框架与库详解)
|
||||
3. [技术栈集成架构](#技术栈集成架构)
|
||||
4. [依赖关系与版本兼容性](#依赖关系与版本兼容性)
|
||||
5. [配置管理机制](#配置管理机制)
|
||||
6. [扩展点与优化建议](#扩展点与优化建议)
|
||||
|
||||
## 技术选型概览
|
||||
|
||||
CarrotSkin项目采用现代化的Go语言技术栈,构建了一个高性能、可扩展的Minecraft皮肤站后端服务。项目技术选型遵循"简单、高效、可靠"的原则,选择了业界广泛使用且维护良好的开源库。整体技术架构采用分层设计,从前端API路由到后端数据存储,各组件职责清晰,耦合度低。
|
||||
|
||||
项目以Gin作为Web框架,提供了高性能的HTTP路由和中间件支持;使用GORM作为ORM框架,简化了与PostgreSQL数据库的交互;通过Redis实现高速缓存和会话管理;采用MinIO/RustFS作为S3兼容的对象存储,用于存储用户上传的皮肤和头像文件;权限控制由Casbin提供,支持灵活的基于角色的访问控制(RBAC);日志系统采用Zap,提供结构化日志记录;配置管理使用Viper,支持从环境变量加载配置;API文档通过Swag生成,提供交互式文档界面。
|
||||
|
||||
这种技术组合不仅满足了项目当前的功能需求,还为未来的扩展和维护提供了良好的基础。所有技术栈均通过Go Modules进行依赖管理,确保了版本的确定性和可重现性。
|
||||
|
||||
**本节来源**
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [run.sh](file://run.sh#L1-L37)
|
||||
|
||||
## 核心框架与库详解
|
||||
|
||||
### Gin(Web框架)
|
||||
|
||||
Gin是CarrotSkin项目的核心Web框架,负责处理所有HTTP请求和响应。项目通过`internal/handler/routes.go`文件中的`RegisterRoutes`函数注册了完整的API路由体系,包括用户认证、材质管理、档案服务等模块。Gin的中间件机制被充分利用,实现了JWT认证、CORS跨域、请求日志记录和panic恢复等关键功能。
|
||||
|
||||
在`internal/middleware/auth.go`中,实现了`AuthMiddleware`函数,作为JWT认证中间件,验证请求头中的Authorization令牌,并将用户信息注入到请求上下文中。同时,`internal/middleware/cors.go`提供了CORS中间件,配置了允许的源、方法和头部,确保前端应用能够正常访问API。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Client["客户端请求"] --> CORS["CORS中间件"]
|
||||
CORS --> Logger["日志中间件"]
|
||||
Logger --> Recovery["恢复中间件"]
|
||||
Recovery --> Auth["认证中间件"]
|
||||
Auth --> Router["Gin路由器"]
|
||||
Router --> Handler["业务处理函数"]
|
||||
Handler --> Response["响应客户端"]
|
||||
```
|
||||
|
||||
**本节来源**
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
- [cors.go](file://internal/middleware/cors.go#L1-L23)
|
||||
|
||||
### GORM(ORM)
|
||||
|
||||
GORM作为对象关系映射框架,负责与PostgreSQL数据库的交互。在`pkg/database/postgres.go`中,`New`函数创建了GORM数据库实例,通过`gorm.Open`连接到PostgreSQL。项目配置了连接池参数,包括最大空闲连接数、最大打开连接数和连接最大生命周期,以优化数据库性能。
|
||||
|
||||
GORM的配置中禁用了外键约束自动创建(`DisableForeignKeyConstraintWhenMigrating: true`),以避免循环依赖问题。每个数据模型(如用户、材质、档案等)都定义了对应的GORM模型结构体,并通过`TableName`方法指定数据库表名。GORM的链式API使得数据库查询操作简洁明了,支持CRUD操作、关联查询和事务处理。
|
||||
|
||||
**本节来源**
|
||||
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [audit_log.go](file://internal/model/audit_log.go#L24-L45)
|
||||
|
||||
### PostgreSQL(数据库)
|
||||
|
||||
PostgreSQL作为关系型数据库,存储了用户信息、权限规则、系统配置等结构化数据。项目通过`pkg/config/config.go`中的`DatabaseConfig`结构体定义了数据库连接配置,包括主机、端口、用户名、密码、数据库名、SSL模式和时区等。默认配置连接到本地的PostgreSQL实例,时区设置为"Asia/Shanghai"。
|
||||
|
||||
数据库设计遵循规范化原则,通过外键关联不同实体。例如,用户表与材质表、档案表之间存在一对多关系。Casbin权限规则存储在`casbin_rule`表中,通过GORM适配器与Casbin库集成。审计日志记录在`audit_logs`表中,用于追踪关键操作。
|
||||
|
||||
**本节来源**
|
||||
- [config.go](file://pkg/config/config.go#L34-L47)
|
||||
|
||||
### Redis(缓存)
|
||||
|
||||
Redis在项目中扮演着多重角色:缓存验证码、存储会话数据、计数器等。`pkg/redis/redis.go`中的`Client`结构体包装了`github.com/redis/go-redis/v9`客户端,提供了类型安全的方法封装。`New`函数创建Redis连接,并配置了连接超时、读写超时和连接池大小。
|
||||
|
||||
Redis客户端支持多种数据结构操作,包括字符串(Set/Get)、哈希(HSet/HGet)、集合(SAdd/SMembers)和有序集合(ZAdd/ZRange)。在用户服务中,Redis用于存储验证码,通过`Set`方法设置带过期时间的键值对;在Yggdrasil服务中,用于存储服务器加入会话,确保Minecraft服务器验证的安全性。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class RedisClient {
|
||||
+Client *redis.Client
|
||||
+logger *zap.Logger
|
||||
+Set(ctx, key, value, expiration) error
|
||||
+Get(ctx, key) (string, error)
|
||||
+HSet(ctx, key, values) error
|
||||
+HGet(ctx, key, field) (string, error)
|
||||
+SAdd(ctx, key, members) error
|
||||
+SMembers(ctx, key) ([]string, error)
|
||||
+Close() error
|
||||
}
|
||||
class RedisConfig {
|
||||
+Host string
|
||||
+Port int
|
||||
+Password string
|
||||
+Database int
|
||||
+PoolSize int
|
||||
}
|
||||
RedisClient --> RedisConfig : "使用"
|
||||
```
|
||||
|
||||
**本节来源**
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
|
||||
### MinIO/RustFS(对象存储)
|
||||
|
||||
MinIO/RustFS作为S3兼容的对象存储,用于存储用户上传的皮肤文件和头像。`pkg/storage/minio.go`中的`StorageClient`结构体封装了`github.com/minio/minio-go/v7`客户端,支持所有S3兼容的存储服务。`NewStorage`函数创建存储客户端,通过`minio.New`连接到指定的Endpoint。
|
||||
|
||||
项目采用预签名URL(Presigned URL)机制,实现安全的文件上传。`GeneratePresignedURL`方法生成一个带有时限的PUT URL,前端可以直接使用该URL上传文件到指定的存储桶,而无需经过后端服务器中转,大大减轻了服务器带宽压力。`GeneratePresignedPostURL`方法支持表单上传,提供了更灵活的上传方式。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Frontend as 前端
|
||||
participant Backend as 后端
|
||||
participant Storage as 对象存储
|
||||
Frontend->>Backend : 请求上传URL
|
||||
Backend->>Storage : 生成预签名URL
|
||||
Storage-->>Backend : 返回预签名URL
|
||||
Backend-->>Frontend : 返回预签名URL
|
||||
Frontend->>Storage : 直接上传文件
|
||||
Storage-->>Frontend : 上传成功
|
||||
```
|
||||
|
||||
**本节来源**
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
- [config.go](file://pkg/config/config.go#L58-L65)
|
||||
|
||||
### Casbin(权限控制)
|
||||
|
||||
Casbin提供基于模型的访问控制(ABAC),支持多种权限模型,如ACL、RBAC、ABAC等。CarrotSkin项目采用RBAC(基于角色的访问控制)模型,定义在`configs/casbin/rbac_model.conf`文件中。该模型包含请求定义、策略定义、角色定义、匹配器和策略效果。
|
||||
|
||||
在RBAC模型中,权限由主体(用户)、客体(资源)和动作(操作)三元组组成。通过`g(r.sub, p.sub)`表达式,实现了角色继承,即用户可以继承其角色的权限。权限策略存储在数据库的`casbin_rule`表中,通过GORM适配器与Casbin集成,实现了动态权限管理。
|
||||
|
||||
**本节来源**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf#L1-L15)
|
||||
- [audit_log.go](file://internal/model/audit_log.go#L29-L45)
|
||||
|
||||
### Zap(日志)
|
||||
|
||||
Zap是Uber开源的高性能日志库,提供结构化日志记录。`pkg/logger/logger.go`中的`New`函数根据配置创建Zap日志实例,支持JSON和控制台两种输出格式。日志级别可配置为debug、info、warn或error,默认为info。
|
||||
|
||||
日志记录包含时间戳、日志级别、消息以及结构化字段,如HTTP请求的方法、路径、状态码、延迟、客户端IP和用户代理。日志输出可以重定向到文件,支持按大小轮转,最大保留28天的日志文件。在中间件中,Zap被用于记录每个HTTP请求的详细信息,便于问题排查和性能分析。
|
||||
|
||||
**本节来源**
|
||||
- [logger.go](file://pkg/logger/logger.go#L1-L69)
|
||||
- [config.go](file://pkg/config/config.go#L79-L88)
|
||||
|
||||
### Viper(配置管理)
|
||||
|
||||
Viper是Go语言的配置管理库,支持多种配置源和格式。CarrotSkin项目完全从环境变量加载配置,不依赖配置文件。`pkg/config/config.go`中的`Load`函数使用Viper从环境变量中解析配置,环境变量前缀为"CARROTSKIN"。
|
||||
|
||||
项目定义了`Config`结构体,包含服务器、数据库、Redis、对象存储、JWT、Casbin、日志、上传和邮件等模块的配置。通过`viper.SetDefault`设置默认值,确保在环境变量缺失时仍能正常运行。`setupEnvMappings`函数绑定环境变量到配置字段,实现了灵活的配置映射。
|
||||
|
||||
**本节来源**
|
||||
- [config.go](file://pkg/config/config.go#L13-L305)
|
||||
|
||||
### Swag(API文档)
|
||||
|
||||
Swag通过解析Go代码中的注释,自动生成Swagger/OpenAPI规范。`internal/handler/swagger.go`文件包含API文档的元信息,如标题、版本、描述、联系人、许可证和主机地址。每个API端点通过注释定义,包括路径、方法、参数、请求体、响应和安全定义。
|
||||
|
||||
`run.sh`脚本中的`swag init`命令生成Swagger文档,输出到`docs`目录。`SetupSwagger`函数注册Swagger UI路由,使开发者可以通过浏览器访问交互式API文档。API文档不仅提高了开发效率,还为前端团队提供了清晰的接口契约。
|
||||
|
||||
**本节来源**
|
||||
- [swagger.go](file://internal/handler/swagger.go#L1-L63)
|
||||
- [run.sh](file://run.sh#L22-L27)
|
||||
|
||||
## 技术栈集成架构
|
||||
|
||||
CarrotSkin项目的技术栈集成体现了清晰的分层架构和关注点分离原则。整个系统可以分为四层:API层、服务层、数据访问层和外部服务层。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["API层"] --> B["服务层"]
|
||||
B --> C["数据访问层"]
|
||||
C --> D["外部服务层"]
|
||||
subgraph API层
|
||||
A1[Gin Web框架]
|
||||
A2[路由注册]
|
||||
A3[中间件]
|
||||
A4[Swagger文档]
|
||||
end
|
||||
subgraph 服务层
|
||||
B1[用户服务]
|
||||
B2[材质服务]
|
||||
B3[档案服务]
|
||||
B4[验证码服务]
|
||||
end
|
||||
subgraph 数据访问层
|
||||
C1[GORM ORM]
|
||||
C2[PostgreSQL]
|
||||
C3[Redis客户端]
|
||||
C4[对象存储客户端]
|
||||
end
|
||||
subgraph 外部服务层
|
||||
D1[PostgreSQL数据库]
|
||||
D2[Redis服务器]
|
||||
D3[MinIO/RustFS]
|
||||
end
|
||||
A1 --> B1
|
||||
A1 --> B2
|
||||
A1 --> B3
|
||||
A1 --> B4
|
||||
B1 --> C1
|
||||
B2 --> C1
|
||||
B3 --> C1
|
||||
B4 --> C3
|
||||
C1 --> D1
|
||||
C3 --> D2
|
||||
C4 --> D3
|
||||
```
|
||||
|
||||
API层由Gin框架驱动,处理HTTP请求,执行中间件链(CORS、日志、认证、恢复),然后将请求分发到相应的服务层。服务层包含业务逻辑,协调多个数据访问组件完成特定功能。数据访问层封装了与外部服务的交互细节,提供统一的接口给服务层使用。外部服务层是独立部署的基础设施,通过网络与应用层通信。
|
||||
|
||||
这种架构的优势在于:各层之间松耦合,便于独立开发和测试;服务层专注于业务逻辑,不关心数据存储细节;数据访问层可以轻松替换底层实现(如从PostgreSQL迁移到MySQL);外部服务的故障隔离,不会直接影响应用核心逻辑。
|
||||
|
||||
**本节来源**
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
|
||||
## 依赖关系与版本兼容性
|
||||
|
||||
项目的依赖关系通过Go Modules进行管理,`go.mod`文件明确指定了所有直接依赖的版本。核心依赖的版本选择考虑了稳定性、性能和功能需求:
|
||||
|
||||
- **Gin v1.9.1**:稳定版本,提供了完整的Web框架功能
|
||||
- **GORM v1.25.5**:支持PostgreSQL的最新特性,如JSONB类型
|
||||
- **PostgreSQL驱动 v1.5.4**:GORM官方推荐的PostgreSQL驱动
|
||||
- **Redis客户端 v9.0.5**:支持Redis 6+的新特性,如ACL
|
||||
- **MinIO Go SDK v7.0.66**:支持S3兼容存储的最新API
|
||||
- **Casbin v1.16.2**:提供了丰富的权限模型和适配器
|
||||
- **Zap v1.26.0**:高性能日志库,支持结构化日志
|
||||
- **Viper v1.21.0**:功能完整的配置管理库
|
||||
- **Swag v1.16.2**:支持最新Swagger规范
|
||||
|
||||
所有依赖都通过`require`指令声明,间接依赖由Go Modules自动解析。版本号采用语义化版本控制,确保了向后兼容性。项目使用Go 1.23.0版本,与所有依赖库兼容。
|
||||
|
||||
依赖的初始化遵循单例模式,通过`sync.Once`确保只初始化一次。例如,`pkg/auth/manager.go`中的`Init`函数使用`sync.Once`保证JWT服务只创建一次,避免了并发初始化问题。这种设计模式提高了系统的稳定性和资源利用率。
|
||||
|
||||
**本节来源**
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [manager.go](file://pkg/auth/manager.go#L1-L46)
|
||||
|
||||
## 配置管理机制
|
||||
|
||||
CarrotSkin项目采用环境变量作为唯一的配置源,不使用配置文件,这符合12-Factor应用的方法论。`pkg/config/config.go`中的`Load`函数是配置管理的核心,它首先加载`.env`文件(如果存在),然后从环境变量中解析配置。
|
||||
|
||||
配置结构体`Config`包含所有模块的配置,通过`mapstructure`标签映射到Viper的键。`setDefaults`函数设置合理的默认值,确保应用在最小配置下也能运行。`setupEnvMappings`函数显式绑定环境变量到配置字段,提高了配置的可读性和可维护性。
|
||||
|
||||
敏感配置(如数据库密码、JWT密钥)必须通过环境变量提供,避免硬编码在代码中。`start.sh`脚本展示了生产环境的配置示例,包括数据库、Redis、对象存储和JWT的详细配置。这种配置方式便于在不同环境(开发、测试、生产)之间切换,只需更改环境变量即可。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["配置源"] --> B[".env文件"]
|
||||
A --> C["环境变量"]
|
||||
B --> D["加载.env文件"]
|
||||
C --> E["读取环境变量"]
|
||||
D --> F["设置默认值"]
|
||||
E --> F
|
||||
F --> G["解析到Config结构体"]
|
||||
G --> H["覆盖特定配置"]
|
||||
H --> I["返回配置实例"]
|
||||
```
|
||||
|
||||
**本节来源**
|
||||
- [config.go](file://pkg/config/config.go#L108-L305)
|
||||
- [start.sh](file://start.sh#L1-L41)
|
||||
|
||||
## 扩展点与优化建议
|
||||
|
||||
CarrotSkin项目的技术栈为未来的扩展提供了良好的基础。以下是几个潜在的扩展点和优化建议:
|
||||
|
||||
1. **数据库读写分离**:随着用户量增长,可以引入数据库读写分离,将读请求分发到只读副本,减轻主库压力。GORM支持多数据库连接,可以轻松实现。
|
||||
|
||||
2. **缓存策略优化**:当前Redis主要用于验证码和会话,可以扩展为数据缓存层,缓存热点数据(如用户资料、材质列表),减少数据库查询。
|
||||
|
||||
3. **消息队列集成**:对于耗时操作(如邮件发送、文件处理),可以引入消息队列(如RabbitMQ、Kafka),实现异步处理,提高响应速度。
|
||||
|
||||
4. **监控与告警**:集成Prometheus和Grafana,收集应用性能指标(如请求延迟、错误率、数据库连接数),设置告警规则,及时发现和解决问题。
|
||||
|
||||
5. **容器化部署**:将应用打包为Docker镜像,使用Kubernetes进行编排,实现高可用和弹性伸缩。
|
||||
|
||||
6. **多存储后端支持**:当前对象存储基于S3兼容协议,可以扩展支持其他云存储(如AWS S3、阿里云OSS),提供存储后端选择。
|
||||
|
||||
7. **权限模型增强**:在RBAC基础上,引入ABAC(基于属性的访问控制),实现更细粒度的权限控制。
|
||||
|
||||
8. **API版本管理**:随着API演进,引入版本管理(如`/api/v2`),确保向后兼容,平滑过渡。
|
||||
|
||||
这些扩展点可以在不影响现有功能的前提下逐步实施,持续提升系统的性能、可靠性和可维护性。
|
||||
|
||||
**本节来源**
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
@@ -1,546 +0,0 @@
|
||||
# 数据模型
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/model/user.go](file://internal/model/user.go)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go)
|
||||
- [internal/model/audit_log.go](file://internal/model/audit_log.go)
|
||||
- [internal/model/token.go](file://internal/model/token.go)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向 CarrotSkin 项目的数据库层,聚焦于核心实体 User、Texture、Profile 和 SystemConfig 的数据模型设计。内容涵盖字段定义、数据类型、主键/外键关系、索引与约束、业务语义与验证规则,并提供实体关系图(ERD)、示例数据与数据访问模式说明,帮助初学者快速理解,同时为有经验的开发者提供性能优化与迁移策略建议。
|
||||
|
||||
## 项目结构
|
||||
围绕数据模型的关键目录与文件:
|
||||
- 模型层:internal/model 下的各实体模型文件
|
||||
- 仓储层:internal/repository 下的 CRUD 与聚合查询实现
|
||||
- 数据库层:pkg/database 提供连接、迁移与连接池管理
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "模型层"
|
||||
MUser["User<br/>internal/model/user.go"]
|
||||
MTexture["Texture<br/>internal/model/texture.go"]
|
||||
MProfile["Profile<br/>internal/model/profile.go"]
|
||||
MSystemConfig["SystemConfig<br/>internal/model/system_config.go"]
|
||||
MAudit["AuditLog<br/>internal/model/audit_log.go"]
|
||||
MToken["Token<br/>internal/model/token.go"]
|
||||
MYgg["Yggdrasil<br/>internal/model/yggdrasil.go"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
RUser["UserRepository<br/>internal/repository/user_repository.go"]
|
||||
RTexture["TextureRepository<br/>internal/repository/texture_repository.go"]
|
||||
RProfile["ProfileRepository<br/>internal/repository/profile_repository.go"]
|
||||
RSystem["SystemConfigRepository<br/>internal/repository/system_config_repository.go"]
|
||||
end
|
||||
subgraph "数据库层"
|
||||
DManager["Database Manager<br/>pkg/database/manager.go"]
|
||||
DPostgres["Postgres Driver<br/>pkg/database/postgres.go"]
|
||||
end
|
||||
MUser --> RUser
|
||||
MTexture --> RTexture
|
||||
MProfile --> RProfile
|
||||
MSystemConfig --> RSystem
|
||||
RUser --> DManager
|
||||
RTexture --> DManager
|
||||
RProfile --> DManager
|
||||
RSystem --> DManager
|
||||
DManager --> DPostgres
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
- [internal/model/audit_log.go](file://internal/model/audit_log.go#L1-L46)
|
||||
- [internal/model/token.go](file://internal/model/token.go#L1-L15)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L48)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
|
||||
章节来源
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
|
||||
## 核心组件
|
||||
本节对四个核心实体进行字段级说明,包括数据类型、约束、索引与业务含义。
|
||||
|
||||
- User(用户)
|
||||
- 主键:id(自增整数)
|
||||
- 唯一索引:username、email
|
||||
- 字段要点:
|
||||
- username:字符串,唯一,用于登录与标识
|
||||
- email:字符串,唯一,用于找回密码与通知
|
||||
- avatar:字符串,头像 URL
|
||||
- points:整数,积分余额,支持增减与日志追踪
|
||||
- role:字符串,默认“user”,用于权限控制
|
||||
- status:小整数,1 正常、0 禁用、-1 删除(软删除)
|
||||
- properties:JSONB,存储扩展属性
|
||||
- last_login_at:时间戳,最近登录时间
|
||||
- created_at/updated_at:时间戳,默认 CURRENT_TIMESTAMP
|
||||
- 业务规则:
|
||||
- 软删除通过 status 字段实现
|
||||
- 登录日志与积分日志分别记录在 user_login_logs 与 user_point_logs
|
||||
|
||||
- Texture(材质)
|
||||
- 主键:id(自增整数)
|
||||
- 外键:uploader_id → User.id
|
||||
- 唯一索引:hash(SHA-256)
|
||||
- 字段要点:
|
||||
- uploader_id:整数,上传者
|
||||
- name/description:名称与描述
|
||||
- type:枚举,SKIN 或 CAPE
|
||||
- url:字符串,材质资源地址
|
||||
- hash:字符串(64),唯一,用于去重
|
||||
- size:整数,字节数
|
||||
- is_public:布尔,是否公开
|
||||
- download_count/favorite_count:整数,统计指标,带索引
|
||||
- is_slim:布尔,是否 Alex(细)模型
|
||||
- status:小整数,1 正常、0 审核中、-1 删除(软删除)
|
||||
- created_at/updated_at:时间戳
|
||||
- 业务规则:
|
||||
- is_public + type + status 组合索引用于检索
|
||||
- 下载与收藏计数采用表达式更新,避免并发竞争导致的丢失更新
|
||||
|
||||
- Profile(档案)
|
||||
- 主键:uuid(字符串,36 位)
|
||||
- 外键:user_id → User.id
|
||||
- 唯一索引:name(角色名)
|
||||
- 字段要点:
|
||||
- uuid:档案 UUID
|
||||
- user_id:整数,所属用户
|
||||
- name:字符串,Minecraft 角色名,唯一
|
||||
- skin_id/cape_id:整数,关联 Texture.id
|
||||
- rsa_private_key:文本,RSA 私钥(不对外返回)
|
||||
- is_active:布尔,是否为当前激活档案
|
||||
- last_used_at:时间戳,最近使用时间
|
||||
- created_at/updated_at:时间戳
|
||||
- 业务规则:
|
||||
- 激活档案切换时,同一用户下其他档案会被置为非激活
|
||||
- 支持预加载 Skin/Cape 关联实体
|
||||
|
||||
- SystemConfig(系统配置)
|
||||
- 主键:id(自增整数)
|
||||
- 唯一索引:key
|
||||
- 字段要点:
|
||||
- key:字符串,配置键,唯一
|
||||
- value:文本,配置值
|
||||
- description:字符串,描述
|
||||
- type:枚举,STRING/INTEGER/BOOLEAN/JSON
|
||||
- is_public:布尔,是否允许前端读取
|
||||
- created_at/updated_at:时间戳
|
||||
- 业务规则:
|
||||
- is_public 为 true 的配置可作为公开响应的一部分返回
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L28-L71)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L42-L77)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L32)
|
||||
|
||||
## 架构总览
|
||||
数据库层通过连接管理器统一初始化与迁移,模型定义通过 GORM 注解映射到 PostgreSQL 表结构。迁移顺序遵循“先被引用表,后引用表”的原则,确保外键约束可用。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as "应用启动"
|
||||
participant DBMgr as "Database Manager"
|
||||
participant PG as "Postgres Driver"
|
||||
participant ORM as "GORM"
|
||||
participant Repo as "Repositories"
|
||||
App->>DBMgr : Init(cfg, logger)
|
||||
DBMgr->>PG : New(cfg)
|
||||
PG-->>DBMgr : *gorm.DB
|
||||
DBMgr->>ORM : AutoMigrate(models...)
|
||||
ORM-->>DBMgr : 迁移完成
|
||||
App->>Repo : 使用仓储层进行CRUD
|
||||
Repo->>ORM : 执行查询/更新
|
||||
ORM-->>Repo : 结果
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L99)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 实体关系图(ERD)
|
||||
以下 ERD 展示了 User、Texture、Profile、SystemConfig 的主键、外键与关键索引。
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER {
|
||||
bigint id PK
|
||||
varchar username UK
|
||||
varchar email UK
|
||||
varchar avatar
|
||||
integer points
|
||||
varchar role
|
||||
smallint status
|
||||
jsonb properties
|
||||
timestamp last_login_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
TEXTURE {
|
||||
bigint id PK
|
||||
bigint uploader_id FK
|
||||
varchar name
|
||||
text description
|
||||
varchar type
|
||||
varchar url
|
||||
varchar hash UK
|
||||
integer size
|
||||
boolean is_public
|
||||
integer download_count
|
||||
integer favorite_count
|
||||
boolean is_slim
|
||||
smallint status
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
PROFILE {
|
||||
varchar uuid PK
|
||||
bigint user_id FK
|
||||
varchar name UK
|
||||
bigint skin_id
|
||||
bigint cape_id
|
||||
text rsa_private_key
|
||||
boolean is_active
|
||||
timestamp last_used_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
SYSTEM_CONFIG {
|
||||
bigint id PK
|
||||
varchar key UK
|
||||
text value
|
||||
varchar description
|
||||
varchar type
|
||||
boolean is_public
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
USER ||--o{ TEXTURE : "uploader_id -> id"
|
||||
USER ||--o{ PROFILE : "user_id -> id"
|
||||
TEXTURE ||--o{ PROFILE : "skin_id/cape_id -> id"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L7-L21)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L32)
|
||||
|
||||
### User 模型与访问模式
|
||||
- 字段与约束
|
||||
- 主键:id
|
||||
- 唯一索引:username、email
|
||||
- 状态软删除:status=-1 表示删除
|
||||
- JSONB 扩展属性:properties
|
||||
- 关联与日志
|
||||
- UserPointLog:记录积分变动,含 operator_id 关联操作人
|
||||
- UserLoginLog:记录登录来源与结果
|
||||
- 仓储能力
|
||||
- 基础 CRUD、按用户名/邮箱查询、软删除
|
||||
- 事务内更新积分并写入日志
|
||||
- 更新头像、邮箱等字段
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Svc as "服务层"
|
||||
participant Repo as "UserRepository"
|
||||
participant DB as "GORM"
|
||||
participant Log as "UserPointLog"
|
||||
Svc->>Repo : UpdateUserPoints(userID, amount, type, reason)
|
||||
Repo->>DB : 事务开始
|
||||
Repo->>DB : 查询用户当前积分
|
||||
Repo->>DB : 校验余额防止负值
|
||||
Repo->>DB : 更新用户积分
|
||||
Repo->>DB : 创建积分日志(Log)
|
||||
DB-->>Repo : 提交事务
|
||||
Repo-->>Svc : 返回结果
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L28-L71)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L7-L71)
|
||||
|
||||
### Texture 模型与访问模式
|
||||
- 字段与约束
|
||||
- 主键:id;唯一索引:hash
|
||||
- 外键:uploader_id → User.id
|
||||
- 组合索引:is_public + type + status;download_count/favorite_count 带索引
|
||||
- 关联与日志
|
||||
- UserTextureFavorite:用户收藏材质(联合唯一索引 uk_user_texture)
|
||||
- TextureDownloadLog:下载记录
|
||||
- 仓储能力
|
||||
- 创建、按 ID/Hash 查询、分页与搜索(关键词、类型、公开度)
|
||||
- 软删除(status=-1)
|
||||
- 表达式更新下载/收藏计数,避免竞态
|
||||
- 收藏/取消收藏与查询用户收藏列表
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["搜索入口"]) --> BuildQuery["构建基础查询<br/>status=1"]
|
||||
BuildQuery --> Public{"仅公开?"}
|
||||
Public --> |是| ApplyPublic["追加 is_public=true"]
|
||||
Public --> |否| SkipPublic["跳过公开过滤"]
|
||||
ApplyPublic --> TypeFilter{"指定类型?"}
|
||||
SkipPublic --> TypeFilter
|
||||
TypeFilter --> |是| ApplyType["追加 type=?"]
|
||||
TypeFilter --> |否| KeywordFilter
|
||||
ApplyType --> KeywordFilter{"有关键词?"}
|
||||
KeywordFilter --> |是| ApplyKeyword["name/description LIKE %keyword%"]
|
||||
KeywordFilter --> |否| Paginate
|
||||
ApplyKeyword --> Paginate["COUNT + ORDER BY created_at DESC + 分页"]
|
||||
Paginate --> End(["返回结果"])
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
### Profile 模型与访问模式
|
||||
- 字段与约束
|
||||
- 主键:uuid;唯一索引:name
|
||||
- 外键:user_id → User.id;skin_id/cape_id → Texture.id
|
||||
- is_active 控制当前生效档案
|
||||
- 仓储能力
|
||||
- 创建、按 uuid/name 查询、按用户查询全部档案
|
||||
- 设置激活档案(事务内将用户其他档案置为非激活)
|
||||
- 更新最后使用时间
|
||||
- KeyPair 的读写(JSONB 字段)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Svc as "服务层"
|
||||
participant Repo as "ProfileRepository"
|
||||
participant DB as "GORM"
|
||||
Svc->>Repo : SetActiveProfile(uuid, userID)
|
||||
Repo->>DB : 事务开始
|
||||
Repo->>DB : 将用户所有档案 is_active=false
|
||||
Repo->>DB : 将指定档案 is_active=true
|
||||
DB-->>Repo : 提交事务
|
||||
Repo-->>Svc : 返回结果
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
### SystemConfig 模型与访问模式
|
||||
- 字段与约束
|
||||
- 主键:id;唯一索引:key
|
||||
- 类型枚举:STRING/INTEGER/BOOLEAN/JSON
|
||||
- is_public 控制前端可见性
|
||||
- 仓储能力
|
||||
- 按 key 查询、获取公开配置、获取全部配置、更新值
|
||||
|
||||
章节来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
|
||||
### 其他相关模型与迁移顺序
|
||||
- AuditLog:审计日志,记录用户行为与资源变更,含 JSONB 字段与多维索引
|
||||
- Token:认证令牌模型(表名为 token),用于 Yggdrasil 等流程
|
||||
- Yggdrasil:与 User 一对一关联,User 创建后自动同步生成随机密码记录
|
||||
|
||||
迁移顺序(AutoMigrate):
|
||||
- 先创建被引用表:User、UserPointLog、UserLoginLog
|
||||
- 再创建引用表:Profile、Texture、UserTextureFavorite、TextureDownloadLog、Token、Yggdrasil
|
||||
- 最后创建 SystemConfig、AuditLog、CasbinRule
|
||||
|
||||
章节来源
|
||||
- [internal/model/audit_log.go](file://internal/model/audit_log.go#L1-L46)
|
||||
- [internal/model/token.go](file://internal/model/token.go#L1-L15)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L48)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
|
||||
|
||||
## 依赖分析
|
||||
- 模型到仓储:各模型通过 GORM 注解与仓储层交互,仓储层负责具体查询、更新与事务控制
|
||||
- 仓储到数据库:统一通过 MustGetDB 获取连接,避免重复初始化
|
||||
- 迁移顺序:AutoMigrate 明确列出迁移顺序,确保外键约束可用
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
MUser["User Model"] --> RUser["UserRepository"]
|
||||
MTexture["Texture Model"] --> RTexture["TextureRepository"]
|
||||
MProfile["Profile Model"] --> RProfile["ProfileRepository"]
|
||||
MSystem["SystemConfig Model"] --> RSystem["SystemConfigRepository"]
|
||||
RUser --> DMgr["Database Manager"]
|
||||
RTexture --> DMgr
|
||||
RProfile --> DMgr
|
||||
RSystem --> DMgr
|
||||
DMgr --> DPostgres["Postgres Driver"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L35-L99)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
|
||||
## 性能考量
|
||||
- 索引策略
|
||||
- User:username、email 唯一索引;登录/积分日志按 created_at 倒序索引
|
||||
- Texture:hash 唯一索引;is_public + type + status 组合索引;download_count/favorite_count 带索引
|
||||
- Profile:name 唯一索引;user_id 索引
|
||||
- SystemConfig:key 唯一索引;is_public 索引
|
||||
- AuditLog:多维索引(action、resource_type+resource_id、created_at)
|
||||
- 并发与一致性
|
||||
- 使用表达式更新计数字段(download_count、favorite_count),避免竞态
|
||||
- 事务内更新用户积分并写入日志,保证原子性
|
||||
- 连接池与日志
|
||||
- 连接池参数由配置注入,建议结合负载压测调整 MaxOpenConns、MaxIdleConns、ConnMaxLifetime
|
||||
- 日志级别在 Postgres 驱动中按驱动类型配置
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L28-L71)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L15-L40)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L42-L77)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L7-L29)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L17-L32)
|
||||
- [internal/model/audit_log.go](file://internal/model/audit_log.go#L7-L27)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
|
||||
## 故障排查指南
|
||||
- 数据库未初始化
|
||||
- 现象:调用 MustGetDB 抛错或 AutoMigrate 返回错误
|
||||
- 排查:确认已调用 Init(cfg, logger),检查配置项与连接可达性
|
||||
- 迁移失败
|
||||
- 现象:AutoMigrate 报错
|
||||
- 排查:检查迁移顺序是否正确(先被引用表,后引用表);确认数据库版本与驱动兼容
|
||||
- 查询不到记录
|
||||
- 现象:按 username/email/uuid 查询返回空
|
||||
- 排查:确认 status 非 -1;检查唯一索引是否冲突;确认大小写与格式
|
||||
- 并发计数不一致
|
||||
- 现象:download_count/favorite_count 不准确
|
||||
- 排查:确认使用表达式更新;避免直接赋值覆盖
|
||||
- 事务回滚
|
||||
- 现象:积分更新失败或日志未写入
|
||||
- 排查:检查事务内错误处理与提交路径
|
||||
|
||||
章节来源
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L33)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
|
||||
## 结论
|
||||
本数据模型围绕 User、Texture、Profile、SystemConfig 四大核心实体,采用 PostgreSQL + GORM 的组合实现,具备完善的索引与约束策略、清晰的迁移顺序与事务保障。通过仓储层抽象,实现了稳定的 CRUD 与复杂查询能力。建议在生产环境中结合监控与压测持续优化连接池与索引策略。
|
||||
|
||||
## 附录
|
||||
|
||||
### 示例数据(概念性)
|
||||
- User
|
||||
- id: 1
|
||||
- username: "alice"
|
||||
- email: "alice@example.com"
|
||||
- points: 100
|
||||
- role: "user"
|
||||
- status: 1
|
||||
- properties: "{}"
|
||||
- last_login_at: 当前时间
|
||||
- created_at/updated_at: 当前时间
|
||||
- Texture
|
||||
- id: 101
|
||||
- uploader_id: 1
|
||||
- name: "Steve Classic"
|
||||
- type: "SKIN"
|
||||
- url: "/uploads/skin_101.png"
|
||||
- hash: "sha256..."
|
||||
- size: 102400
|
||||
- is_public: true
|
||||
- download_count: 120
|
||||
- favorite_count: 45
|
||||
- is_slim: false
|
||||
- status: 1
|
||||
- created_at/updated_at: 当前时间
|
||||
- Profile
|
||||
- uuid: "f47ac10b-62d6-4c6f-8b3c-1234567890ab"
|
||||
- user_id: 1
|
||||
- name: "Steve"
|
||||
- skin_id: 101
|
||||
- cape_id: null
|
||||
- rsa_private_key: "..."
|
||||
- is_active: true
|
||||
- last_used_at: 当前时间
|
||||
- created_at/updated_at: 当前时间
|
||||
- SystemConfig
|
||||
- id: 1
|
||||
- key: "site_name"
|
||||
- value: "CarrotSkin"
|
||||
- description: "站点名称"
|
||||
- type: "STRING"
|
||||
- is_public: true
|
||||
- created_at/updated_at: 当前时间
|
||||
|
||||
### 数据访问模式清单
|
||||
- User
|
||||
- 创建/查询/更新/软删除
|
||||
- 事务内更新积分并写入日志
|
||||
- Texture
|
||||
- 创建/查询/按 Hash 去重/分页搜索/软删除
|
||||
- 表达式更新下载/收藏计数
|
||||
- 收藏/取消收藏与查询收藏列表
|
||||
- Profile
|
||||
- 创建/查询/按用户查询/设置激活档案/更新最后使用时间
|
||||
- KeyPair 读写(JSONB)
|
||||
- SystemConfig
|
||||
- 按 key 查询/获取公开配置/获取全部配置/更新值
|
||||
|
||||
章节来源
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
@@ -1,408 +0,0 @@
|
||||
# 材质模型
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/model/texture.go](file://internal/model/texture.go)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql)
|
||||
- [internal/types/common.go](file://internal/types/common.go)
|
||||
- [internal/model/user.go](file://internal/model/user.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件围绕材质模型进行系统化技术文档整理,覆盖以下主题:
|
||||
- Texture 结构体及关联类型(TextureType、UserTextureFavorite、TextureDownloadLog)的字段语义与实现细节
|
||||
- 材质类型(SKIN/CAPE)、哈希值(Hash)、URL 存储、尺寸与 Slim 模型标识的技术实现
|
||||
- 材质状态机(正常、审核中、已删除)与公开性控制(IsPublic)的业务逻辑
|
||||
- 与用户(Uploader)、收藏系统、下载日志的关联关系与索引策略
|
||||
- 材质元数据管理、下载计数器并发更新优化、收藏功能去重机制的实践指导
|
||||
- 基于 GORM 的复杂查询模式示例与最佳实践
|
||||
|
||||
## 项目结构
|
||||
材质模型相关代码分布于模型层、仓储层与服务层,并通过数据库初始化脚本定义表结构与索引。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "模型层"
|
||||
M1["internal/model/texture.go<br/>定义 Texture/TextureType/UserTextureFavorite/TextureDownloadLog"]
|
||||
M2["internal/model/user.go<br/>定义 User 及其关联"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
R1["internal/repository/texture_repository.go<br/>材质 CRUD、搜索、计数器更新、收藏与下载日志"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S1["internal/service/texture_service.go<br/>材质创建/更新/删除/搜索、下载记录、收藏切换、上传限制检查"]
|
||||
end
|
||||
subgraph "数据库"
|
||||
D1["scripts/carrotskin_postgres.sql<br/>表结构、索引、约束、触发器"]
|
||||
end
|
||||
M1 --> R1
|
||||
M2 --> R1
|
||||
S1 --> R1
|
||||
R1 --> D1
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L1-L344)
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L1-L344)
|
||||
|
||||
## 核心组件
|
||||
- Texture:材质主实体,包含上传者、名称、描述、类型、URL、哈希、尺寸、公开性、下载/收藏计数、Slim 标识、状态与时间戳,并与 User 建立外键关联
|
||||
- TextureType:材质类型枚举(SKIN/CAPE)
|
||||
- UserTextureFavorite:用户对材质的收藏关系,具备联合唯一索引以保证去重
|
||||
- TextureDownloadLog:材质下载记录,包含用户、IP、UA、时间等信息,并与 Texture/User 建立外键关联
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L127-L152)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
|
||||
## 架构总览
|
||||
材质模型在服务层完成业务编排,在仓储层封装数据库访问,在模型层定义数据结构与关系。数据库层面通过外键、索引与触发器保障一致性与性能。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Texture {
|
||||
+int64 ID
|
||||
+int64 UploaderID
|
||||
+string Name
|
||||
+string Description
|
||||
+TextureType Type
|
||||
+string URL
|
||||
+string Hash
|
||||
+int Size
|
||||
+bool IsPublic
|
||||
+int DownloadCount
|
||||
+int FavoriteCount
|
||||
+bool IsSlim
|
||||
+int16 Status
|
||||
+time CreatedAt
|
||||
+time UpdatedAt
|
||||
}
|
||||
class User {
|
||||
+int64 ID
|
||||
+string Username
|
||||
+string Email
|
||||
+string Avatar
|
||||
+int Points
|
||||
+string Role
|
||||
+int16 Status
|
||||
+string Properties
|
||||
+time LastLoginAt
|
||||
+time CreatedAt
|
||||
+time UpdatedAt
|
||||
}
|
||||
class UserTextureFavorite {
|
||||
+int64 ID
|
||||
+int64 UserID
|
||||
+int64 TextureID
|
||||
+time CreatedAt
|
||||
}
|
||||
class TextureDownloadLog {
|
||||
+int64 ID
|
||||
+int64 TextureID
|
||||
+*int64 UserID
|
||||
+string IPAddress
|
||||
+string UserAgent
|
||||
+time CreatedAt
|
||||
}
|
||||
Texture --> User : "Uploader"
|
||||
UserTextureFavorite --> User : "User"
|
||||
UserTextureFavorite --> Texture : "Texture"
|
||||
TextureDownloadLog --> Texture : "Texture"
|
||||
TextureDownloadLog --> User : "User"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### Texture 字段与业务语义
|
||||
- 类型与哈希
|
||||
- Type:材质类型,取值为 SKIN 或 CAPE
|
||||
- Hash:材质文件的 SHA-256 哈希,作为全局唯一标识,用于去重与校验
|
||||
- 存储与元数据
|
||||
- URL:材质在对象存储中的访问地址
|
||||
- Size:文件大小(字节)
|
||||
- IsSlim:Slim 模型标识(Alex 细臂为 true,Steve 粗臂为 false)
|
||||
- 公开性与状态
|
||||
- IsPublic:是否公开到皮肤广场
|
||||
- Status:状态机(1 正常、0 审核中、-1 已删除)
|
||||
- 计数器与排序
|
||||
- DownloadCount、FavoriteCount:下载与收藏计数,分别建立降序索引以便高效排序与统计
|
||||
- 时间戳与索引
|
||||
- CreatedAt/UpdatedAt:自动维护
|
||||
- UploaderID、IsPublic+Type+Status、DownloadCount/FavoriteCount 等索引支撑查询与排序
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L85)
|
||||
|
||||
### TextureType 与 Slim 模型
|
||||
- TextureType 枚举在模型与类型定义中保持一致,确保序列化与校验的一致性
|
||||
- Slim 模型标识用于区分 Alex 与 Steve 模型,便于客户端渲染与兼容
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L7-L13)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L127-L152)
|
||||
|
||||
### 状态机与公开性控制
|
||||
- 状态机
|
||||
- 1:正常可用
|
||||
- 0:审核中(可视为待审状态)
|
||||
- -1:已删除(软删除)
|
||||
- 公开性控制
|
||||
- IsPublic 控制是否对外可见
|
||||
- 查询侧影响
|
||||
- 搜索接口默认仅返回状态为正常的材质
|
||||
- 用户上传列表过滤掉已删除材质
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L28-L31)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L93-L103)
|
||||
|
||||
### 与用户的关联关系
|
||||
- 上传者关联
|
||||
- Texture 与 User 通过 UploaderID 外键关联,查询时可预加载上传者信息
|
||||
- 角色档案关联
|
||||
- Profiles 表通过 skin_id/cape_id 引用 textures,形成角色与材质的绑定关系
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L33-L35)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L111-L127)
|
||||
|
||||
### 收藏系统(UserTextureFavorite)
|
||||
- 关系模型
|
||||
- 用户与材质的多对多中间表,字段包含 user_id、texture_id、created_at
|
||||
- 联合唯一索引 uk_user_texture,保证同一用户对同一材质只能收藏一次
|
||||
- 业务流程
|
||||
- 切换收藏:先检查是否已收藏,再执行新增或删除,并同步更新材质的收藏计数
|
||||
- 收藏列表:子查询获取用户收藏的材质ID,再按创建时间倒序分页查询
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Service as "TextureService"
|
||||
participant Repo as "TextureRepository"
|
||||
participant DB as "数据库"
|
||||
Client->>Service : "ToggleTextureFavorite(userID, textureID)"
|
||||
Service->>Repo : "FindTextureByID(textureID)"
|
||||
Repo->>DB : "SELECT * FROM textures WHERE id=?"
|
||||
DB-->>Repo : "Texture"
|
||||
Repo-->>Service : "Texture"
|
||||
Service->>Repo : "IsTextureFavorited(userID, textureID)"
|
||||
Repo->>DB : "SELECT COUNT(*) FROM user_texture_favorites WHERE user_id=? AND texture_id=?"
|
||||
DB-->>Repo : "count"
|
||||
Repo-->>Service : "bool"
|
||||
alt 已收藏
|
||||
Service->>Repo : "RemoveTextureFavorite(userID, textureID)"
|
||||
Repo->>DB : "DELETE FROM user_texture_favorites WHERE user_id=? AND texture_id=?"
|
||||
Service->>Repo : "DecrementTextureFavoriteCount(textureID)"
|
||||
Repo->>DB : "UPDATE textures SET favorite_count=favorite_count-1 WHERE id=?"
|
||||
else 未收藏
|
||||
Service->>Repo : "AddTextureFavorite(userID, textureID)"
|
||||
Repo->>DB : "INSERT INTO user_texture_favorites(user_id, texture_id)"
|
||||
Service->>Repo : "IncrementTextureFavoriteCount(textureID)"
|
||||
Repo->>DB : "UPDATE textures SET favorite_count=favorite_count+1 WHERE id=?"
|
||||
end
|
||||
Service-->>Client : "返回收藏状态"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L201)
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L42-L57)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L201)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
|
||||
|
||||
### 下载日志(TextureDownloadLog)
|
||||
- 记录每次下载的材质、用户、IP、UA、时间等信息
|
||||
- 下载计数器采用 GORM 表达式更新,避免并发写冲突导致的计数不准
|
||||
- 提供按时间倒序索引,便于统计与审计
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Service as "TextureService"
|
||||
participant Repo as "TextureRepository"
|
||||
participant DB as "数据库"
|
||||
Client->>Service : "RecordTextureDownload(textureID, userID, ip, ua)"
|
||||
Service->>Repo : "FindTextureByID(textureID)"
|
||||
Repo->>DB : "SELECT * FROM textures WHERE id=?"
|
||||
DB-->>Repo : "Texture"
|
||||
Service->>Repo : "IncrementTextureDownloadCount(textureID)"
|
||||
Repo->>DB : "UPDATE textures SET download_count=download_count+1 WHERE id=?"
|
||||
Service->>Repo : "CreateTextureDownloadLog(log)"
|
||||
Repo->>DB : "INSERT INTO texture_download_logs(texture_id, user_id, ip_address, user_agent)"
|
||||
Service-->>Client : "成功"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L157)
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L59-L76)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L157)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L271-L291)
|
||||
|
||||
### GORM 使用示例与复杂查询模式
|
||||
- 创建材质
|
||||
- 参数校验与去重:先检查用户存在与哈希唯一性,再创建材质
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
- 搜索材质
|
||||
- 过滤条件:状态=正常、公开筛选、类型筛选、关键词模糊匹配
|
||||
- 分页与总数:先 Count 再分页查询,支持预加载上传者
|
||||
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
|
||||
- 更新材质
|
||||
- 权限校验:仅上传者可更新
|
||||
- 动态字段更新:根据传入字段选择性更新
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L141)
|
||||
- 删除材质(软删除)
|
||||
- 仅将状态置为 -1,保留历史数据与日志
|
||||
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L131)
|
||||
- 收藏切换
|
||||
- 原子性:先判断是否已收藏,再执行新增/删除并更新计数
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- 收藏列表
|
||||
- 子查询:先查出用户收藏的材质ID集合,再查询材质并分页
|
||||
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221)
|
||||
- 上传限制检查
|
||||
- 统计用户已上传材质数量,与系统配置的最大值比较
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L252)
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L141)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L131)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L252)
|
||||
|
||||
## 依赖分析
|
||||
- 模型层依赖
|
||||
- Texture 依赖 TextureType、User
|
||||
- UserTextureFavorite 依赖 User、Texture
|
||||
- TextureDownloadLog 依赖 User、Texture
|
||||
- 仓储层依赖
|
||||
- 通过数据库连接池访问 textures、user_texture_favorites、texture_download_logs
|
||||
- 使用 GORM 的表达式更新计数器,避免并发竞争
|
||||
- 服务层依赖
|
||||
- 负责业务规则编排:权限校验、去重、状态机、计数器更新、日志记录
|
||||
- 数据库层
|
||||
- 外键约束:textures.uploader_id -> user.id;favorites 与 logs 对 textures/user 的引用
|
||||
- 索引:textures 上的多列索引与计数器降序索引;favorites/logs 上的单列索引
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Service["TextureService"] --> Repo["TextureRepository"]
|
||||
Repo --> Model["Model: Texture/User/Favorite/DownloadLog"]
|
||||
Repo --> DB["PostgreSQL 表: textures/favorites/logs"]
|
||||
Model --> DB
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L110)
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L110)
|
||||
|
||||
## 性能考虑
|
||||
- 并发计数器更新
|
||||
- 下载计数与收藏计数均使用 GORM 表达式更新,避免读取-计算-写回的竞态,降低锁竞争
|
||||
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
- 索引设计
|
||||
- textures 表的多列索引(is_public, type, status)与计数器降序索引(download_count/favorite_count)支撑高频查询与排序
|
||||
- favorites/logs 表的关键列建立索引,提升收藏去重与日志检索效率
|
||||
- 参考路径:[scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L85)
|
||||
- 分页与总数
|
||||
- 搜索与收藏列表先 Count 再分页查询,避免一次性加载大量数据
|
||||
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
|
||||
- 预加载
|
||||
- 查询时预加载上传者信息,减少 N+1 查询风险
|
||||
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L18-L21)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L85)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
|
||||
|
||||
## 故障排查指南
|
||||
- 材质不存在或已被删除
|
||||
- 查询时若返回空或状态为已删除,服务层会抛出明确错误
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
|
||||
- 权限不足
|
||||
- 更新/删除材质需校验上传者身份,否则返回权限错误
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L120)
|
||||
- 哈希重复
|
||||
- 创建材质前检查哈希唯一性,重复则拒绝创建
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31)
|
||||
- 收藏去重失败
|
||||
- 联合唯一索引 uk_user_texture 保证同一用户对同一材质仅一条收藏记录
|
||||
- 若出现重复插入,需检查业务层是否正确先查询后插入
|
||||
- 参考路径:[internal/model/texture.go](file://internal/model/texture.go#L42-L57),[scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
|
||||
- 下载计数不准确
|
||||
- 确认使用表达式更新而非普通更新,避免并发写入导致计数偏差
|
||||
- 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L120)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L42-L57)
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
|
||||
## 结论
|
||||
材质模型通过清晰的数据结构、严格的外键与索引设计、以及基于 GORM 的并发安全更新,实现了高性能、可扩展且易维护的材质管理能力。状态机与公开性控制使内容治理更加灵活,收藏与下载日志完善了用户行为追踪与运营分析基础。建议在后续迭代中持续关注索引命中率与查询计划,配合缓存与异步任务进一步优化热点查询与批量操作。
|
||||
|
||||
## 附录
|
||||
- 字段与索引对照
|
||||
- textures:uploader_id、is_public+type+status、download_count、favorite_count、hash 唯一索引
|
||||
- user_texture_favorites:user_id、texture_id、created_at、uk_user_texture
|
||||
- texture_download_logs:texture_id、user_id、ip_address、created_at
|
||||
- 关键查询模式
|
||||
- 搜索与分页:先 Count 再分页,支持关键词、类型、公开性筛选
|
||||
- 收藏列表:子查询 + 分页 + 预加载
|
||||
- 并发计数:表达式更新,避免竞态
|
||||
|
||||
章节来源
|
||||
- [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L110)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
@@ -1,473 +0,0 @@
|
||||
# 档案模型
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/model/profile.go](file://internal/model/profile.go)
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go)
|
||||
- [internal/model/user.go](file://internal/model/user.go)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go)
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
|
||||
- [internal/types/common.go](file://internal/types/common.go)
|
||||
- [internal/service/profile_service_test.go](file://internal/service/profile_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件围绕Minecraft档案模型进行系统化技术文档整理,重点覆盖以下主题:
|
||||
- Profile结构体的核心字段:UUID、Name、SkinID、CapeID以及RSA密钥对的安全存储机制
|
||||
- 档案激活状态(IsActive)与最后使用时间(LastUsedAt)的业务意义
|
||||
- 与用户(User)、皮肤(Texture)的外键关联关系及其在Yggdrasil协议中的作用
|
||||
- ProfileResponse响应结构的设计原理,包括Textures数据的嵌套格式与元数据(metadata)中模型类型(slim/classic)的表示方式
|
||||
- UUID命名规范、角色名唯一性约束以及密钥轮换策略的技术说明,并结合实际API响应示例进行说明
|
||||
|
||||
## 项目结构
|
||||
本项目采用分层架构,档案模型位于内部模型层,服务层负责业务流程编排,仓储层负责数据持久化,类型定义用于请求/响应契约与校验。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "模型层"
|
||||
M_Profile["Profile<br/>profiles 表"]
|
||||
M_Texture["Texture<br/>textures 表"]
|
||||
M_User["User<br/>user 表"]
|
||||
M_Ygg["Yggdrasil<br/>Yggdrasil 表"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S_Profile["ProfileService<br/>档案业务逻辑"]
|
||||
S_Ygg["YggdrasilService<br/>Yggdrasil协议集成"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
R_Profile["ProfileRepository<br/>档案数据访问"]
|
||||
end
|
||||
M_Profile --> M_User
|
||||
M_Profile --> M_Texture
|
||||
S_Profile --> R_Profile
|
||||
S_Ygg --> R_Profile
|
||||
S_Ygg --> M_Profile
|
||||
S_Ygg --> M_Ygg
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
|
||||
章节来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
|
||||
## 核心组件
|
||||
- Profile:Minecraft档案实体,包含UUID、角色名、皮肤/披风ID、RSA私钥、激活状态、最后使用时间等字段,并与User、Texture建立关联。
|
||||
- ProfileResponse:对外响应结构,包含UUID、角色名、Textures(含皮肤/披风URL与metadata模型类型)、IsActive、LastUsedAt、CreatedAt。
|
||||
- KeyPair:密钥对结构,包含私钥、公钥、过期时间、刷新时间,用于安全存储与轮换。
|
||||
- Texture:材质实体,支持皮肤/披风类型、URL、哈希、是否公开、下载/收藏计数、是否slim等属性。
|
||||
- Yggdrasil:与用户绑定的Yggdrasil密码实体,用于协议认证与会话管理。
|
||||
|
||||
章节来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
|
||||
## 架构总览
|
||||
档案模型贯穿“模型-服务-仓储-外部协议”的完整链路。服务层负责创建/更新/删除档案、设置活跃档案、更新最后使用时间、生成RSA密钥对;仓储层负责数据库读写与事务控制;模型层定义表结构与关联;类型层定义请求/响应契约;Yggdrasil服务层负责与外部协议交互(如会话数据存储与校验)。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Handler as "处理器"
|
||||
participant Service as "ProfileService"
|
||||
participant Repo as "ProfileRepository"
|
||||
participant DB as "数据库"
|
||||
participant Ygg as "YggdrasilService"
|
||||
Client->>Handler : "创建档案/更新档案/设置活跃档案"
|
||||
Handler->>Service : "调用业务方法"
|
||||
Service->>Repo : "查询/更新/事务"
|
||||
Repo->>DB : "执行SQL/GORM操作"
|
||||
DB-->>Repo : "返回结果"
|
||||
Repo-->>Service : "返回实体/影响行数"
|
||||
Service-->>Handler : "返回业务结果"
|
||||
Handler-->>Client : "返回ProfileResponse/错误"
|
||||
Note over Service,DB : "设置活跃档案时,服务层会调用仓储层更新最后使用时间"
|
||||
Service->>Ygg : "JoinServer/HasJoinedServer与Yggdrasil协议交互"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### Profile结构体与外键关联
|
||||
- 字段说明
|
||||
- UUID:档案唯一标识,主键,长度为36(标准UUID格式)
|
||||
- Name:角色名,最大16字符,全局唯一索引
|
||||
- SkinID/CapeID:指向Texture表的外键,允许为空
|
||||
- RSAPrivateKey:RSA私钥(PEM格式),不返回给前端
|
||||
- IsActive:是否为当前活跃档案,默认true,带索引
|
||||
- LastUsedAt:最后使用时间,用于统计与排序
|
||||
- CreatedAt/UpdatedAt:记录创建与更新时间戳
|
||||
- 关联关系
|
||||
- Profile.UserID -> User.ID
|
||||
- Profile.SkinID -> Texture.ID
|
||||
- Profile.CapeID -> Texture.ID
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Profile {
|
||||
+string UUID
|
||||
+int64 UserID
|
||||
+string Name
|
||||
+int64* SkinID
|
||||
+int64* CapeID
|
||||
+string RSAPrivateKey
|
||||
+bool IsActive
|
||||
+time.Time* LastUsedAt
|
||||
+time.Time CreatedAt
|
||||
+time.Time UpdatedAt
|
||||
}
|
||||
class User {
|
||||
+int64 ID
|
||||
+string Username
|
||||
+string Email
|
||||
+string Role
|
||||
+int16 Status
|
||||
+time.Time* LastLoginAt
|
||||
+time.Time CreatedAt
|
||||
+time.Time UpdatedAt
|
||||
}
|
||||
class Texture {
|
||||
+int64 ID
|
||||
+int64 UploaderID
|
||||
+string Name
|
||||
+string URL
|
||||
+string Hash
|
||||
+bool IsPublic
|
||||
+int DownloadCount
|
||||
+int FavoriteCount
|
||||
+bool IsSlim
|
||||
+int16 Status
|
||||
+time.Time CreatedAt
|
||||
+time.Time UpdatedAt
|
||||
}
|
||||
Profile --> User : "外键 UserID"
|
||||
Profile --> Texture : "外键 SkinID"
|
||||
Profile --> Texture : "外键 CapeID"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
章节来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
### ProfileResponse响应结构设计
|
||||
- 结构组成
|
||||
- uuid/name/is_active/last_used_at/created_at:基础档案信息
|
||||
- textures:包含皮肤与披风两个子项
|
||||
- SKIN/CAPE:每个项包含url与metadata
|
||||
- metadata.model:取值为"slim"或"classic",用于指示模型类型
|
||||
- 设计原则
|
||||
- 以Yggdrasil协议兼容为目标,textures字段直接映射皮肤/披风资源与元数据
|
||||
- 通过枚举化的model字段明确区分Alex(细臂)与Steve(粗臂)模型
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ProfileResponse {
|
||||
+string uuid
|
||||
+string name
|
||||
+ProfileTexturesData textures
|
||||
+bool is_active
|
||||
+time.Time* last_used_at
|
||||
+time.Time created_at
|
||||
}
|
||||
class ProfileTexturesData {
|
||||
+ProfileTexture* SKIN
|
||||
+ProfileTexture* CAPE
|
||||
}
|
||||
class ProfileTexture {
|
||||
+string url
|
||||
+ProfileTextureMetadata* metadata
|
||||
}
|
||||
class ProfileTextureMetadata {
|
||||
+string model
|
||||
}
|
||||
ProfileResponse --> ProfileTexturesData
|
||||
ProfileTexturesData --> ProfileTexture
|
||||
ProfileTexture --> ProfileTextureMetadata
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L31-L64)
|
||||
|
||||
章节来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L31-L64)
|
||||
|
||||
### RSA密钥对的安全存储机制
|
||||
- 生成与存储
|
||||
- 服务层在创建档案时生成RSA-2048私钥(PEM格式),并保存至Profile.RSAPrivateKey字段
|
||||
- 私钥不返回给前端,避免泄露风险
|
||||
- 读取与轮换
|
||||
- 仓储层提供GetProfileKeyPair/UpdateProfileKeyPair接口,支持从数据库读取与更新密钥对
|
||||
- KeyPair结构体包含私钥、公钥、过期时间、刷新时间,便于后续密钥轮换策略落地
|
||||
- 安全建议
|
||||
- 建议在密钥过期前主动轮换,更新数据库中的密钥对并同步到缓存/内存
|
||||
- 对敏感字段进行最小暴露,仅在必要时解密或传输
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> Gen["生成RSA-2048私钥PEM"]
|
||||
Gen --> Save["保存至Profile.RSAPrivateKey"]
|
||||
Save --> Use["对外响应不返回私钥"]
|
||||
Use --> Rotate{"是否需要轮换?"}
|
||||
Rotate --> |否| End(["结束"])
|
||||
Rotate --> |是| Update["更新数据库中的密钥对"]
|
||||
Update --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L204-L220)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L139-L199)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
章节来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L204-L220)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L139-L199)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
### 档案激活状态与最后使用时间的业务意义
|
||||
- IsActive
|
||||
- 用于标记当前用户所选中的活跃档案,同一用户下仅有一个档案处于活跃状态
|
||||
- 设置活跃档案时,服务层会将该用户其他档案置为非活跃
|
||||
- LastUsedAt
|
||||
- 每当设置活跃档案时,服务层会更新该字段为当前时间
|
||||
- 用于统计与排序,帮助用户快速识别最近使用的档案
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Service as "ProfileService"
|
||||
participant Repo as "ProfileRepository"
|
||||
participant DB as "数据库"
|
||||
Client->>Service : "设置活跃档案"
|
||||
Service->>Repo : "将用户其他档案置为非活跃"
|
||||
Repo->>DB : "UPDATE profiles SET is_active=false WHERE user_id=?"
|
||||
Service->>Repo : "将目标档案置为活跃"
|
||||
Repo->>DB : "UPDATE profiles SET is_active=true WHERE uuid=? AND user_id=?"
|
||||
Service->>Repo : "更新最后使用时间"
|
||||
Repo->>DB : "UPDATE profiles SET last_used_at=CURRENT_TIMESTAMP WHERE uuid=?"
|
||||
DB-->>Repo : "返回影响行数"
|
||||
Repo-->>Service : "返回成功"
|
||||
Service-->>Client : "返回成功"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
|
||||
章节来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L89-L117)
|
||||
|
||||
### 与用户(User)与皮肤(Texture)的外键关联关系
|
||||
- Profile.UserID -> User.ID
|
||||
- 一对多:一个用户可拥有多个档案
|
||||
- Profile.SkinID/CapeID -> Texture.ID
|
||||
- 多对一:一个档案可关联到一张皮肤与一张披风(均可为空)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER {
|
||||
int64 id PK
|
||||
string username UK
|
||||
string email UK
|
||||
string role
|
||||
int16 status
|
||||
timestamp last_login_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
TEXTURE {
|
||||
int64 id PK
|
||||
int64 uploader_id FK
|
||||
string name
|
||||
string url
|
||||
string hash UK
|
||||
bool is_public
|
||||
int download_count
|
||||
int favorite_count
|
||||
bool is_slim
|
||||
int16 status
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
PROFILE {
|
||||
string uuid PK
|
||||
int64 user_id FK
|
||||
string name UK
|
||||
int64* skin_id FK
|
||||
int64* cape_id FK
|
||||
text rsa_private_key
|
||||
bool is_active
|
||||
timestamp last_used_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
USER ||--o{ PROFILE : "拥有"
|
||||
TEXTURE ||--o{ PROFILE : "被使用"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
### 在Yggdrasil协议中的作用
|
||||
- Profile与Yggdrasil的关系
|
||||
- ProfileResponse中的textures字段用于向客户端提供皮肤/披风资源与模型元数据,满足Yggdrasil协议对纹理与模型类型的要求
|
||||
- Yggdrasil实体与User存在一对一关联,用于协议认证与会话管理
|
||||
- 会话与验证
|
||||
- 服务层提供JoinServer/HasJoinedServer方法,将会话数据写入Redis并进行用户名/IP校验,确保玩家加入服务器的合法性
|
||||
- 会话数据包含accessToken、userName、selectedProfile、ip等关键字段
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant YggSvc as "YggdrasilService"
|
||||
participant TokenRepo as "TokenRepository"
|
||||
participant ProfRepo as "ProfileRepository"
|
||||
participant Redis as "Redis"
|
||||
Client->>YggSvc : "JoinServer(serverId, accessToken, selectedProfile, ip)"
|
||||
YggSvc->>TokenRepo : "根据accessToken查询Token"
|
||||
TokenRepo-->>YggSvc : "返回Token"
|
||||
YggSvc->>ProfRepo : "根据ProfileId查询Profile"
|
||||
ProfRepo-->>YggSvc : "返回Profile"
|
||||
YggSvc->>Redis : "写入会话数据Join_前缀+serverId"
|
||||
Redis-->>YggSvc : "成功"
|
||||
YggSvc-->>Client : "返回成功"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
|
||||
章节来源
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
- [internal/model/yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
|
||||
### UUID命名规范、角色名唯一性约束与密钥轮换策略
|
||||
- UUID命名规范
|
||||
- Profile.UUID为主键,采用标准36字符格式(包含连字符)
|
||||
- 服务层在创建档案时使用标准库生成新UUID
|
||||
- 角色名唯一性约束
|
||||
- Profile.Name具有唯一索引,服务层在创建/更新时均进行冲突检测
|
||||
- 请求/响应契约中对名称长度有严格限制(1-16字符)
|
||||
- 密钥轮换策略
|
||||
- KeyPair结构体提供过期时间与刷新时间字段,便于实现周期性轮换
|
||||
- 建议在过期前主动生成新密钥对并更新数据库,同时同步到缓存/内存,确保服务可用性
|
||||
|
||||
章节来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L181-L206)
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L18-L69)
|
||||
- [internal/service/profile_service_test.go](file://internal/service/profile_service_test.go#L348-L406)
|
||||
|
||||
## 依赖分析
|
||||
- 组件耦合
|
||||
- ProfileService高度依赖ProfileRepository与数据库/GORM
|
||||
- ProfileRepository对数据库连接与事务有直接依赖
|
||||
- YggdrasilService依赖Redis与TokenRepository,间接依赖ProfileRepository
|
||||
- 关联关系
|
||||
- Profile与User/Texture通过GORM外键注解建立关联
|
||||
- ProfileResponse与Profile/Texture的嵌套结构映射清晰,便于序列化/反序列化
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
ProfileService --> ProfileRepository
|
||||
ProfileRepository --> Database["GORM/PostgreSQL"]
|
||||
YggdrasilService --> Redis["Redis"]
|
||||
YggdrasilService --> ProfileRepository
|
||||
Profile --> User
|
||||
Profile --> Texture
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
|
||||
章节来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [internal/service/yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
|
||||
## 性能考虑
|
||||
- 索引优化
|
||||
- Profile.Name具备唯一索引,减少重复角色名查询成本
|
||||
- Profile.UserID与IsActive具备索引,提升活跃档案切换与查询效率
|
||||
- 预加载策略
|
||||
- 仓储层在查询档案时预加载Skin/Cape关联,避免N+1查询
|
||||
- 事务与一致性
|
||||
- 设置活跃档案采用事务,确保原子性与一致性
|
||||
- 缓存与会话
|
||||
- Yggdrasil会话数据写入Redis,降低频繁查询数据库的压力
|
||||
|
||||
## 故障排查指南
|
||||
- 常见错误与定位
|
||||
- 角色名冲突:创建/更新时若返回“角色名已被使用”,检查Profile.Name唯一性约束与服务层校验逻辑
|
||||
- 权限不足:操作他人档案会返回“无权操作此档案”,检查服务层对Profile.UserID与传入userID的比对
|
||||
- 档案不存在:查询/更新/删除时若返回“档案不存在”,检查UUID格式与仓储层查询条件
|
||||
- 密钥读取失败:GetProfileKeyPair返回“未找到”或错误,检查数据库字段映射与查询条件
|
||||
- 调试建议
|
||||
- 在服务层与仓储层增加日志输出,定位具体环节(查询、更新、事务)
|
||||
- 使用单元测试验证请求/响应契约与边界条件(名称长度、UUID格式、密钥PEM格式)
|
||||
|
||||
章节来源
|
||||
- [internal/service/profile_service.go](file://internal/service/profile_service.go#L71-L159)
|
||||
- [internal/repository/profile_repository.go](file://internal/repository/profile_repository.go#L139-L199)
|
||||
- [internal/service/profile_service_test.go](file://internal/service/profile_service_test.go#L348-L406)
|
||||
|
||||
## 结论
|
||||
本档案模型围绕Profile为核心,结合ProfileResponse的纹理与元数据设计,满足Minecraft Yggdrasil协议对皮肤/披风与模型类型的要求。通过严格的唯一性约束、索引优化与事务保障,确保了数据一致性与性能。RSA密钥对的安全存储与KeyPair结构为后续密钥轮换提供了基础。与User/Texture的外键关联清晰地刻画了用户与材质的使用关系,配合Yggdrasil服务层的会话管理,形成完整的档案生命周期闭环。
|
||||
|
||||
## 附录
|
||||
- API响应示例(概念性说明)
|
||||
- ProfileResponse示例包含uuid、name、textures(含SKIN/CAPE)、is_active、last_used_at、created_at等字段
|
||||
- textures.metadata.model取值为"slim"或"classic",用于指示模型类型
|
||||
- 请求/响应契约要点
|
||||
- UpdateProfileRequest对name长度与skin_id/cape_id进行约束
|
||||
- CreateProfileRequest对角色名长度与类型进行约束
|
||||
|
||||
章节来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L31-L64)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L181-L206)
|
||||
@@ -1,451 +0,0 @@
|
||||
# 用户模型
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/model/user.go](file://internal/model/user.go)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go)
|
||||
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go)
|
||||
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go)
|
||||
- [internal/repository/user_repository_test.go](file://internal/repository/user_repository_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件聚焦于用户模型 User 的字段定义、数据类型、GORM 标签与业务含义,以及与 UserPointLog、UserLoginLog 的关联关系。同时提供用户状态流转图、积分系统使用模式、属性扩展(Properties JSONB)的实践建议,并给出数据验证规则、唯一性约束与索引优化建议,最后结合代码路径说明如何安全地处理密码存储与敏感信息脱敏。
|
||||
|
||||
## 项目结构
|
||||
围绕用户模型的关键文件分布如下:
|
||||
- 模型层:用户、积分日志、登录日志、档案等
|
||||
- 仓储层:用户 CRUD、积分更新、登录日志创建等
|
||||
- 服务层:注册、登录、修改密码、更换邮箱等业务流程
|
||||
- 安全层:密码哈希与校验
|
||||
- 数据库层:PostgreSQL 连接与配置
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "模型层"
|
||||
M_User["User<br/>internal/model/user.go"]
|
||||
M_PointLog["UserPointLog<br/>internal/model/user.go"]
|
||||
M_LoginLog["UserLoginLog<br/>internal/model/user.go"]
|
||||
M_Profile["Profile<br/>internal/model/profile.go"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
R_UserRepo["UserRepository<br/>internal/repository/user_repository.go"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S_UserSvc["UserService<br/>internal/service/user_service.go"]
|
||||
S_Serialize["SerializeService<br/>internal/service/serialize_service.go"]
|
||||
end
|
||||
subgraph "安全层"
|
||||
A_Password["Password Hash/Check<br/>pkg/auth/password.go"]
|
||||
end
|
||||
subgraph "数据库层"
|
||||
D_Postgres["PostgreSQL连接<br/>pkg/database/postgres.go"]
|
||||
end
|
||||
S_UserSvc --> R_UserRepo
|
||||
R_UserRepo --> D_Postgres
|
||||
S_UserSvc --> A_Password
|
||||
S_Serialize --> M_User
|
||||
M_User --> M_PointLog
|
||||
M_User --> M_LoginLog
|
||||
M_Profile --> M_User
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
|
||||
|
||||
## 核心组件
|
||||
本节对 User 结构体及其关键字段进行逐项解析,涵盖字段类型、GORM 标签、默认值、业务语义与约束。
|
||||
|
||||
- 字段与类型
|
||||
- ID:整型自增主键,用于唯一标识用户
|
||||
- Username:字符串,长度限制与唯一索引约束
|
||||
- Password:字符串,存储哈希后的密码,JSON 中不返回
|
||||
- Email:字符串,长度限制与唯一索引约束
|
||||
- Avatar:字符串,头像 URL,默认空串
|
||||
- Points:整型,积分余额,默认 0
|
||||
- Role:字符串,角色,默认 "user"
|
||||
- Status:短整型,状态枚举,默认 1(正常),0(禁用),-1(删除)
|
||||
- Properties:字符串,映射 PostgreSQL 的 JSONB 类型,用于扩展属性
|
||||
- LastLoginAt:时间戳,最近登录时间
|
||||
- CreatedAt/UpdatedAt:时间戳,默认 CURRENT_TIMESTAMP
|
||||
|
||||
- 约束与索引
|
||||
- Username、Email 唯一索引
|
||||
- UserPointLog、UserLoginLog 的 UserID、IP 地址、is_success 等字段建立索引
|
||||
- Properties 以 JSONB 存储,便于灵活扩展
|
||||
|
||||
- 业务含义
|
||||
- 注册时默认 Role 为 "user",Status 为 1(正常),Points 为 0
|
||||
- 登录成功后更新 LastLoginAt
|
||||
- 删除采用软删除,将 Status 设为 -1
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L26)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L28-L70)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L111-L121)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L71-L75)
|
||||
|
||||
## 架构总览
|
||||
用户模型在系统中的交互流程如下:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Handler as "处理器/控制器"
|
||||
participant Service as "UserService"
|
||||
participant Repo as "UserRepository"
|
||||
participant DB as "数据库(PostgreSQL)"
|
||||
participant Auth as "Password(哈希/校验)"
|
||||
Client->>Handler : "注册/登录请求"
|
||||
Handler->>Service : "调用业务方法"
|
||||
Service->>Auth : "注册时哈希密码"
|
||||
Auth-->>Service : "返回哈希值"
|
||||
Service->>Repo : "创建用户/保存用户"
|
||||
Repo->>DB : "执行SQL"
|
||||
DB-->>Repo : "返回结果"
|
||||
Repo-->>Service : "返回用户对象"
|
||||
Service-->>Handler : "返回用户与令牌"
|
||||
Handler-->>Client : "响应(不含敏感字段)"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L11-L15)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### User 结构体与字段详解
|
||||
- ID
|
||||
- 类型:整型自增主键
|
||||
- 用途:全局唯一标识
|
||||
- GORM 标签:主键、自增
|
||||
- Username
|
||||
- 类型:字符串,长度限制,唯一索引
|
||||
- 用途:登录凭据之一
|
||||
- 约束:唯一性
|
||||
- Password
|
||||
- 类型:字符串,存储 bcrypt 哈希
|
||||
- 用途:登录凭据
|
||||
- 安全:JSON 中不返回
|
||||
- Email
|
||||
- 类型:字符串,长度限制,唯一索引
|
||||
- 用途:找回密码、通知等
|
||||
- 约束:唯一性
|
||||
- Avatar
|
||||
- 类型:字符串,URL
|
||||
- 默认:空串
|
||||
- Points
|
||||
- 类型:整型
|
||||
- 默认:0
|
||||
- 用途:积分系统余额
|
||||
- Role
|
||||
- 类型:字符串
|
||||
- 默认:"user"
|
||||
- Status
|
||||
- 类型:短整型
|
||||
- 默认:1(正常)
|
||||
- 取值:1 正常;0 禁用;-1 删除(软删)
|
||||
- Properties
|
||||
- 类型:字符串,映射 PostgreSQL 的 JSONB
|
||||
- 用途:扩展属性,如权限、配置等
|
||||
- LastLoginAt
|
||||
- 类型:时间戳
|
||||
- 用途:记录最近登录时间
|
||||
- CreatedAt/UpdatedAt
|
||||
- 类型:时间戳,默认 CURRENT_TIMESTAMP
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L26)
|
||||
|
||||
### User 与 UserPointLog 的关联
|
||||
- 外键关系
|
||||
- UserPointLog.UserID 引用 User.ID
|
||||
- UserPointLog.OperatorID 可选,指向操作者用户
|
||||
- 级联行为
|
||||
- 代码中未显式声明级联删除/更新,遵循 GORM 默认行为
|
||||
- 由于 User 采用软删除(Status=-1),建议在业务层避免直接删除用户,防止破坏积分日志的完整性
|
||||
- 日志字段
|
||||
- ChangeType:变更类型(如 EARN、SPEND、ADMIN_ADJUST)
|
||||
- Amount/BalanceBefore/BalanceAfter:金额与前后余额
|
||||
- Reason/ReferenceType/ReferenceID:原因与关联对象类型/ID
|
||||
- OperatorID:可选,记录管理员操作者
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class User {
|
||||
+int64 ID
|
||||
+string Username
|
||||
+string Email
|
||||
+string Avatar
|
||||
+int Points
|
||||
+string Role
|
||||
+int16 Status
|
||||
+string Properties
|
||||
+time.Time LastLoginAt
|
||||
+time.Time CreatedAt
|
||||
+time.Time UpdatedAt
|
||||
}
|
||||
class UserPointLog {
|
||||
+int64 ID
|
||||
+int64 UserID
|
||||
+string ChangeType
|
||||
+int Amount
|
||||
+int BalanceBefore
|
||||
+int BalanceAfter
|
||||
+string Reason
|
||||
+string ReferenceType
|
||||
+*int64 ReferenceID
|
||||
+*int64 OperatorID
|
||||
+time.Time CreatedAt
|
||||
}
|
||||
class UserLoginLog {
|
||||
+int64 ID
|
||||
+int64 UserID
|
||||
+string IPAddress
|
||||
+string UserAgent
|
||||
+string LoginMethod
|
||||
+bool IsSuccess
|
||||
+string FailureReason
|
||||
+time.Time CreatedAt
|
||||
}
|
||||
User "1" <-- "many" UserPointLog : "外键UserID"
|
||||
User "1" <-- "many" UserLoginLog : "外键UserID"
|
||||
User "1" <-- "many" UserPointLog : "外键OperatorID(可选)"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L28-L70)
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L28-L70)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
|
||||
|
||||
### User 与 UserLoginLog 的关联
|
||||
- 外键关系
|
||||
- UserLoginLog.UserID 引用 User.ID
|
||||
- 日志字段
|
||||
- IPAddress:inet 类型,记录登录 IP
|
||||
- UserAgent:文本
|
||||
- LoginMethod:默认 PASSWORD
|
||||
- IsSuccess/FailureReason:登录成功与否及原因
|
||||
- 索引
|
||||
- UserID、IPAddress、IsSuccess 建有索引,CreatedAt 建有复合索引并按降序排序
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L52-L70)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L77-L87)
|
||||
|
||||
### 用户状态流转图
|
||||
- 正常(1):可登录、可消费积分
|
||||
- 禁用(0):禁止登录,不影响积分
|
||||
- 删除(-1):软删除,查询时默认过滤该状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 正常
|
||||
正常 --> 禁用 : "管理员操作"
|
||||
正常 --> 删除 : "软删除"
|
||||
禁用 --> 正常 : "恢复"
|
||||
禁用 --> 删除 : "软删除"
|
||||
删除 --> 正常 : "不可恢复"
|
||||
删除 --> 禁用 : "不可恢复"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L14-L16)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L71-L75)
|
||||
|
||||
### 积分系统(Points)使用模式
|
||||
- 初始化:注册时 Points 默认 0
|
||||
- 更新:通过 UpdateUserPoints 在事务中完成,先读取当前余额,计算新余额,再写入并创建日志
|
||||
- 校验:若新余额小于 0,则回滚并返回错误
|
||||
- 日志:记录变更类型、金额、前后余额、原因、关联对象与操作者
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> ReadUser["读取用户当前积分"]
|
||||
ReadUser --> Calc["计算新余额 = 当前 + 变更金额"]
|
||||
Calc --> Check{"新余额 >= 0 ?"}
|
||||
Check --> |否| Rollback["回滚事务并返回错误"]
|
||||
Check --> |是| Update["更新用户积分"]
|
||||
Update --> Log["创建积分日志"]
|
||||
Log --> End(["结束"])
|
||||
Rollback --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
|
||||
|
||||
### 属性扩展(Properties JSONB)使用模式
|
||||
- 存储:Properties 为字符串,映射 PostgreSQL 的 JSONB 类型
|
||||
- 用途:存放扩展字段,如权限、配置等
|
||||
- 序列化:在序列化时可直接返回 Properties,但需注意敏感信息脱敏
|
||||
- 示例路径:序列化用户时将 Properties 原样返回,供上层处理
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L16-L17)
|
||||
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
|
||||
|
||||
### 数据验证规则、唯一性约束与索引优化建议
|
||||
- 验证规则
|
||||
- 用户名非空,长度范围(建议在服务层补充)
|
||||
- 邮箱非空且包含 @ 符号(建议在服务层补充)
|
||||
- 密码非空(建议在服务层补充)
|
||||
- 唯一性约束
|
||||
- Username、Email 唯一索引
|
||||
- 索引优化建议
|
||||
- User:Username、Email 唯一索引(已具备)
|
||||
- UserPointLog:UserID、CreatedAt(降序)、ReferenceType/ReferenceID(可选)
|
||||
- UserLoginLog:UserID、IPAddress、IsSuccess、CreatedAt(降序)
|
||||
- 状态查询
|
||||
- 查询用户时默认排除 Status=-1(软删除)
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L9-L21)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L31-L41)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L55-L61)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L18-L29)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L31-L43)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L45-L57)
|
||||
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go#L109-L164)
|
||||
- [internal/repository/user_repository_test.go](file://internal/repository/user_repository_test.go#L1-L69)
|
||||
|
||||
### 密码存储与敏感信息脱敏
|
||||
- 密码存储
|
||||
- 注册时使用 bcrypt 哈希存储
|
||||
- 登录时使用 bcrypt 校验
|
||||
- User.Password 在 JSON 响应中不返回
|
||||
- 敏感信息脱敏
|
||||
- Password 字段在 JSON 中标记为不输出
|
||||
- Properties 作为 JSONB 返回,需在上层进行脱敏处理(例如移除敏感键或替换为占位符)
|
||||
- 头像 URL、邮箱等字段在返回前可根据需要进行脱敏策略
|
||||
|
||||
章节来源
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L12-L67)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L141-L164)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L10-L12)
|
||||
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
|
||||
|
||||
## 依赖分析
|
||||
- 组件耦合
|
||||
- UserService 依赖 UserRepository、JWT 服务与 Password 工具
|
||||
- UserRepository 依赖数据库连接
|
||||
- User 模型与 UserPointLog、UserLoginLog 通过外键关联
|
||||
- 外键与级联
|
||||
- 未显式声明级联,遵循 GORM 默认行为
|
||||
- 建议在业务层避免直接物理删除用户,以保持积分与登录日志的完整性
|
||||
- 外部依赖
|
||||
- PostgreSQL 驱动与连接池配置
|
||||
- bcrypt 密码哈希
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
S_UserSvc["UserService"] --> R_UserRepo["UserRepository"]
|
||||
S_UserSvc --> A_Password["Password"]
|
||||
R_UserRepo --> D_Postgres["PostgreSQL"]
|
||||
M_User["User"] --> M_PointLog["UserPointLog"]
|
||||
M_User --> M_LoginLog["UserLoginLog"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
|
||||
|
||||
章节来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L70)
|
||||
|
||||
## 性能考虑
|
||||
- 索引设计
|
||||
- User:Username、Email 唯一索引
|
||||
- UserPointLog:UserID、CreatedAt(降序)、ReferenceType/ReferenceID(可选)
|
||||
- UserLoginLog:UserID、IPAddress、IsSuccess、CreatedAt(降序)
|
||||
- 查询过滤
|
||||
- 查询用户时默认排除软删除(Status!=-1),减少扫描
|
||||
- 事务与并发
|
||||
- 积分更新使用事务,保证一致性
|
||||
- 缓存与限流
|
||||
- 登录失败与成功日志可配合缓存与限流策略,降低暴力破解风险
|
||||
|
||||
[本节为通用指导,无需列出具体文件来源]
|
||||
|
||||
## 故障排查指南
|
||||
- 登录失败
|
||||
- 检查用户是否存在与状态是否为 1
|
||||
- 校验密码哈希是否正确
|
||||
- 记录失败日志以便审计
|
||||
- 积分不足
|
||||
- 确认余额计算逻辑与事务一致性
|
||||
- 检查日志是否正确记录
|
||||
- 软删除影响
|
||||
- 查询用户时默认排除 Status=-1
|
||||
- 若出现“用户不存在”,确认是否被软删除
|
||||
|
||||
章节来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L70-L121)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L71-L75)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L89-L124)
|
||||
- [internal/repository/user_repository_test.go](file://internal/repository/user_repository_test.go#L1-L69)
|
||||
|
||||
## 结论
|
||||
User 模型通过明确的字段定义、GORM 标签与业务约束,支撑了注册、登录、积分与日志等核心功能。通过软删除与事务化的积分更新,确保了数据一致性与可审计性。建议在服务层补充输入验证与脱敏策略,并根据业务增长持续优化索引与查询路径。
|
||||
|
||||
[本节为总结性内容,无需列出具体文件来源]
|
||||
|
||||
## 附录
|
||||
- 相关模型与配置
|
||||
- Profile 模型与 User 的关联
|
||||
- SystemConfig 模型(系统配置)
|
||||
- 序列化与脱敏
|
||||
- SerializeUser 返回 Properties,需在上层进行脱敏处理
|
||||
|
||||
章节来源
|
||||
- [internal/model/profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
- [internal/service/serialize_service.go](file://internal/service/serialize_service.go#L86-L97)
|
||||
@@ -1,369 +0,0 @@
|
||||
# 系统配置模型
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
|
||||
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go)
|
||||
- [internal/types/common.go](file://internal/types/common.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [引言](#引言)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 引言
|
||||
本文件围绕系统配置模型进行系统性说明,重点聚焦于 SystemConfig 结构体的设计与应用,涵盖:
|
||||
- 键值配置存储机制与配置类型(ConfigType:STRING/INTEGER/BOOLEAN/JSON)的枚举定义与值序列化策略
|
||||
- IsPublic 标志位如何控制前端可访问的配置项(站点名称、注册开关、维护模式等)
|
||||
- SystemConfigPublicResponse 响应结构的字段映射逻辑与敏感配置与公开配置的分离原则
|
||||
- 关于配置项版本管理、变更审计与缓存策略的建议
|
||||
- 如何安全地读取与更新系统参数,避免配置注入风险
|
||||
|
||||
## 项目结构
|
||||
系统配置模型位于内部模型层与仓库层,配合通用类型与配置加载模块共同构成完整的配置体系。下图展示了与系统配置模型直接相关的文件与职责划分。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "模型层"
|
||||
M1["internal/model/system_config.go<br/>定义 SystemConfig 与 SystemConfigPublicResponse"]
|
||||
end
|
||||
subgraph "仓库层"
|
||||
R1["internal/repository/system_config_repository.go<br/>提供配置查询与更新接口"]
|
||||
R2["internal/repository/system_config_repository_test.go<br/>测试查询条件、公开配置逻辑与更新逻辑"]
|
||||
end
|
||||
subgraph "类型与响应"
|
||||
T1["internal/types/common.go<br/>定义 SystemConfigResponse 等通用响应类型"]
|
||||
end
|
||||
subgraph "配置加载"
|
||||
C1["pkg/config/config.go<br/>应用运行时配置加载与环境变量覆盖"]
|
||||
C2["pkg/config/manager.go<br/>全局配置实例管理"]
|
||||
S1["scripts/check-env.sh<br/>环境变量检查脚本"]
|
||||
end
|
||||
M1 --> R1
|
||||
R1 --> C1
|
||||
T1 -.-> M1
|
||||
C1 --> C2
|
||||
S1 --> C1
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L1-L145)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L61)
|
||||
|
||||
章节来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L61)
|
||||
|
||||
## 核心组件
|
||||
- SystemConfig:系统配置的持久化模型,包含键、值、类型、描述、是否公开、创建与更新时间等字段;通过 GORM 映射到 system_config 表。
|
||||
- ConfigType:配置类型枚举,支持 STRING、INTEGER、BOOLEAN、JSON 四种类型。
|
||||
- IsPublic:布尔标志位,决定该配置是否可被前端获取。
|
||||
- SystemConfigPublicResponse:面向前端的公开配置响应结构,包含站点名称、站点描述、注册开关、维护模式、公告等字段。
|
||||
- SystemConfigRepository:提供按键查询、获取公开配置、获取全部配置、更新配置与更新配置值等操作。
|
||||
- SystemConfigResponse:通用系统配置响应类型(与公开响应不同,包含更多业务参数)。
|
||||
|
||||
章节来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
|
||||
|
||||
## 架构总览
|
||||
系统配置模型遵循“模型-仓库-服务-控制器”的分层设计。配置读写通过仓库层封装数据库访问,对外暴露简洁的接口;公开配置通过 IsPublic 进行过滤,确保敏感信息不泄露给前端。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["前端/管理端"] --> API["控制器/服务层"]
|
||||
API --> Repo["SystemConfigRepository"]
|
||||
Repo --> Model["SystemConfig 模型"]
|
||||
Repo --> DB["数据库 system_config 表"]
|
||||
API --> PublicResp["SystemConfigPublicResponse"]
|
||||
API --> CommonResp["SystemConfigResponse"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L18-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### SystemConfig 结构体与配置类型
|
||||
- 字段说明
|
||||
- Key:配置键,唯一索引,用于标识具体配置项。
|
||||
- Value:配置值,以字符串形式存储,配合 Type 决定解析方式。
|
||||
- Type:配置类型,枚举 STRING/INTEGER/BOOLEAN/JSON。
|
||||
- IsPublic:是否公开,true 时可在公开接口中返回给前端。
|
||||
- Description、CreatedAt、UpdatedAt:元信息与时间戳。
|
||||
- 表名映射:TableName 返回 system_config,用于 GORM 自动迁移与查询。
|
||||
- 配置类型与序列化策略
|
||||
- STRING:直接以字符串存储,适合文本类配置。
|
||||
- INTEGER:字符串存储,需要在读取后转换为整数。
|
||||
- BOOLEAN:字符串存储,需要在读取后转换为布尔值。
|
||||
- JSON:字符串存储,需要在读取后进行 JSON 解析,支持复杂结构。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class SystemConfig {
|
||||
+int64 id
|
||||
+string key
|
||||
+string value
|
||||
+string description
|
||||
+ConfigType type
|
||||
+bool is_public
|
||||
+time created_at
|
||||
+time updated_at
|
||||
+TableName() string
|
||||
}
|
||||
class ConfigType {
|
||||
<<enumeration>>
|
||||
"STRING"
|
||||
"INTEGER"
|
||||
"BOOLEAN"
|
||||
"JSON"
|
||||
}
|
||||
SystemConfig --> ConfigType : "使用"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L32)
|
||||
|
||||
章节来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L32)
|
||||
|
||||
### 公开配置响应 SystemConfigPublicResponse 的字段映射
|
||||
- 字段含义
|
||||
- site_name:站点名称
|
||||
- site_description:站点描述
|
||||
- registration_enabled:注册开关
|
||||
- maintenance_mode:维护模式
|
||||
- announcement:公告
|
||||
- 映射原则
|
||||
- 仅来源于 IsPublic 为 true 的配置项,避免敏感配置泄露。
|
||||
- 字段名与 JSON 序列化标签一一对应,便于前端消费。
|
||||
- 分离原则
|
||||
- 敏感配置(如密钥、内部开关)应设置 IsPublic=false,不在公开响应中出现。
|
||||
- 公开配置(如站点名称、注册开关、维护模式、公告)应设置 IsPublic=true。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class SystemConfigPublicResponse {
|
||||
+string site_name
|
||||
+string site_description
|
||||
+bool registration_enabled
|
||||
+bool maintenance_mode
|
||||
+string announcement
|
||||
}
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L34-L41)
|
||||
|
||||
章节来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L34-L41)
|
||||
|
||||
### 仓库层接口与流程
|
||||
- GetSystemConfigByKey(key):根据键查询配置,若记录不存在返回空而非错误。
|
||||
- GetPublicSystemConfigs():仅返回 IsPublic=true 的配置集合。
|
||||
- GetAllSystemConfigs():返回全部配置(供管理员使用)。
|
||||
- UpdateSystemConfig(config):保存配置(含类型与值)。
|
||||
- UpdateSystemConfigValue(key, value):仅更新值字段。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "调用方"
|
||||
participant Repo as "SystemConfigRepository"
|
||||
participant DB as "数据库"
|
||||
Client->>Repo : GetPublicSystemConfigs()
|
||||
Repo->>DB : 查询 where is_public = true
|
||||
DB-->>Repo : 返回公开配置列表
|
||||
Repo-->>Client : 返回公开配置
|
||||
Client->>Repo : UpdateSystemConfigValue(key, value)
|
||||
Repo->>DB : Model(...).Where("key = ?").Update("value", value)
|
||||
DB-->>Repo : 更新成功
|
||||
Repo-->>Client : 返回 nil
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L57)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
|
||||
|
||||
### 配置读取与序列化策略
|
||||
- 读取流程
|
||||
- 使用 GetSystemConfigByKey(key) 获取配置。
|
||||
- 根据 Type 对 Value 进行类型化解析:
|
||||
- STRING:直接使用字符串。
|
||||
- INTEGER:解析为整数。
|
||||
- BOOLEAN:解析为布尔值。
|
||||
- JSON:解析为结构体或 map。
|
||||
- 序列化策略
|
||||
- 公开响应使用 SystemConfigPublicResponse,仅包含 IsPublic=true 的字段映射。
|
||||
- 非公开配置不参与公开响应序列化。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> Load["GetSystemConfigByKey(key)"]
|
||||
Load --> Found{"找到记录?"}
|
||||
Found --> |否| ReturnNil["返回空配置"]
|
||||
Found --> |是| ParseType["根据 Type 解析 Value"]
|
||||
ParseType --> STRING["STRING:直接使用字符串"]
|
||||
ParseType --> INTEGER["INTEGER:解析为整数"]
|
||||
ParseType --> BOOLEAN["BOOLEAN:解析为布尔值"]
|
||||
ParseType --> JSON["JSON:解析为结构体/字典"]
|
||||
STRING --> End(["结束"])
|
||||
INTEGER --> End
|
||||
BOOLEAN --> End
|
||||
JSON --> End
|
||||
ReturnNil --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L23)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L15)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L23)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L7-L15)
|
||||
|
||||
### 公开配置逻辑与测试验证
|
||||
- 公开配置查询逻辑
|
||||
- GetPublicSystemConfigs() 仅返回 IsPublic=true 的配置,确保敏感配置不被泄露。
|
||||
- 测试覆盖点
|
||||
- 查询条件验证:键非空才视为有效。
|
||||
- 公开配置逻辑:仅包含 IsPublic=true 的配置。
|
||||
- 更新值逻辑:键非空即允许更新,空值亦可接受。
|
||||
- 错误处理:记录不存在时返回空而非错误。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
QStart(["查询入口"]) --> BuildQuery["构建查询:where is_public = true"]
|
||||
BuildQuery --> Exec["执行查询"]
|
||||
Exec --> Result{"查询结果"}
|
||||
Result --> |存在| Filter["过滤 IsPublic=true"]
|
||||
Result --> |不存在| NotFound["返回空集合"]
|
||||
Filter --> Return["返回公开配置集合"]
|
||||
NotFound --> Return
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L33)
|
||||
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L51-L78)
|
||||
|
||||
章节来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L33)
|
||||
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L1-L145)
|
||||
|
||||
### 安全与注入防护
|
||||
- 输入校验
|
||||
- 键非空校验:更新与查询均要求键非空,防止空键导致的异常行为。
|
||||
- 值可为空:允许空值,但需结合业务语义判断有效性。
|
||||
- 类型约束
|
||||
- 通过 Type 字段强制约束 Value 的解析方式,避免任意字符串被错误解读。
|
||||
- 敏感信息隔离
|
||||
- 通过 IsPublic=false 隐藏敏感配置,仅在管理员通道可见。
|
||||
- 环境变量安全
|
||||
- 应用配置加载来自环境变量,建议使用强口令与密钥,脚本会提示 JWT 密钥长度不足的风险。
|
||||
|
||||
章节来源
|
||||
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L80-L116)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L52-L61)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L305)
|
||||
|
||||
## 依赖关系分析
|
||||
- 模型依赖
|
||||
- SystemConfig 依赖 GORM 标签进行数据库映射。
|
||||
- SystemConfigPublicResponse 依赖 JSON 标签进行序列化。
|
||||
- 仓库依赖
|
||||
- 依赖数据库连接(MustGetDB),通过 where 条件实现键查询与公开筛选。
|
||||
- 类型依赖
|
||||
- SystemConfigResponse 与 SystemConfigPublicResponse 分别服务于不同场景的响应结构。
|
||||
- 配置加载依赖
|
||||
- 应用配置加载与环境变量覆盖,确保运行时参数可控且可审计。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Model["SystemConfig 模型"] --> Repo["SystemConfigRepository"]
|
||||
Repo --> DB["数据库"]
|
||||
PublicResp["SystemConfigPublicResponse"] --> API["控制器/服务层"]
|
||||
CommonResp["SystemConfigResponse"] --> API
|
||||
Cfg["pkg/config/config.go"] --> API
|
||||
CfgMgr["pkg/config/manager.go"] --> Cfg
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L18-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
|
||||
|
||||
章节来源
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L18-L41)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L57)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L208-L215)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L1-L63)
|
||||
|
||||
## 性能考量
|
||||
- 查询优化
|
||||
- 为 key 建立唯一索引,提升按键查询效率。
|
||||
- 为 is_public 建立索引,加速公开配置筛选。
|
||||
- 缓存策略建议
|
||||
- 公开配置可引入只读缓存(如 Redis),定期刷新,降低数据库压力。
|
||||
- 对频繁读取的配置项(如站点名称、注册开关、维护模式)设置短 TTL,平衡一致性与性能。
|
||||
- 更新策略
|
||||
- 批量更新时使用 UpdateSystemConfigValue,避免不必要的字段变更导致的冗余日志与锁竞争。
|
||||
- 版本与审计
|
||||
- 建议引入配置版本号字段与审计日志,记录每次变更的时间、操作者与变更内容,便于回溯与合规。
|
||||
|
||||
## 故障排查指南
|
||||
- 记录不存在
|
||||
- GetSystemConfigByKey(key) 在记录不存在时返回空而非错误,调用方需显式判空。
|
||||
- 公开配置为空
|
||||
- 若 GetPublicSystemConfigs() 返回空,检查是否正确设置 IsPublic=true 或数据库中是否存在公开配置。
|
||||
- 更新失败
|
||||
- UpdateSystemConfigValue(key, value) 要求键非空;若更新无效,确认键是否存在且值合法。
|
||||
- 类型解析错误
|
||||
- 根据 Type 对 Value 进行解析;若解析失败,检查配置值格式与类型是否匹配。
|
||||
- 环境变量问题
|
||||
- 使用脚本检查关键环境变量是否缺失或过短(如 JWT 密钥长度),及时修正。
|
||||
|
||||
章节来源
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L11-L23)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L25-L57)
|
||||
- [internal/repository/system_config_repository_test.go](file://internal/repository/system_config_repository_test.go#L119-L145)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L61)
|
||||
|
||||
## 结论
|
||||
SystemConfig 模型通过统一的键值存储与类型约束,实现了灵活而安全的系统配置管理。借助 IsPublic 标志位与公开响应结构,系统在保证功能可用的同时,严格隔离了敏感配置。结合缓存、版本与审计策略,可进一步提升系统的稳定性与可运维性。建议在生产环境中严格执行输入校验、类型解析与注入防护,并建立完善的变更审计流程。
|
||||
|
||||
## 附录
|
||||
- 常用配置键建议
|
||||
- 站点名称:site_name(IsPublic=true)
|
||||
- 站点描述:site_description(IsPublic=true)
|
||||
- 注册开关:registration_enabled(IsPublic=true)
|
||||
- 维护模式:maintenance_mode(IsPublic=true)
|
||||
- 公告:announcement(IsPublic=true)
|
||||
- 管理员专用:secret_key、internal_switch(IsPublic=false)
|
||||
@@ -1,269 +0,0 @@
|
||||
# 服务架构
|
||||
|
||||
<cite>
|
||||
**本文档引用文件**
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [texture_service.go](file://internal/service/texture_service.go)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [user.go](file://internal/model/user.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [服务层职责概述](#服务层职责概述)
|
||||
3. [核心服务详细分析](#核心服务详细分析)
|
||||
4. [服务调用关系与流程](#服务调用关系与流程)
|
||||
5. [业务规则实现](#业务规则实现)
|
||||
6. [错误处理与日志记录](#错误处理与日志记录)
|
||||
7. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
|
||||
CarrotSkin项目的服务层是业务逻辑的核心,负责协调数据访问、执行业务规则并为上层处理器提供接口。本服务架构文档重点介绍`UserService`、`ProfileService`和`TextureService`三大核心服务,详细说明其职责、方法、内部逻辑以及与Repository层的协作方式。文档旨在为初学者提供清晰的服务调用视图,并为经验丰富的开发者提供代码扩展和维护的深入指导。
|
||||
|
||||
## 服务层职责概述
|
||||
|
||||
服务层(位于`internal/service/`目录)是CarrotSkin应用的业务逻辑中枢,其主要职责包括:
|
||||
- **业务逻辑封装**:将复杂的业务规则(如用户注册、档案创建、材质上传)封装在独立的服务方法中。
|
||||
- **数据访问协调**:调用Repository层的方法来持久化或检索数据,实现与数据库的解耦。
|
||||
- **事务管理**:对于涉及多个数据操作的业务(如设置活跃档案),通过Repository层的事务功能保证数据一致性。
|
||||
- **数据验证与转换**:在数据进入或离开系统时进行验证和必要的格式转换。
|
||||
- **外部服务集成**:与认证(JWT)、存储(MinIO)、邮件等外部服务进行交互。
|
||||
|
||||
服务层通过定义清晰的函数接口,为`handler`层提供稳定、可复用的业务能力,是连接用户请求与数据持久化的关键桥梁。
|
||||
|
||||
## 核心服务详细分析
|
||||
|
||||
### UserService 分析
|
||||
|
||||
`UserService`负责管理用户生命周期的核心操作,包括注册、登录、信息更新和密码管理。
|
||||
|
||||
**核心方法与职责**:
|
||||
- `RegisterUser`:处理用户注册流程。该方法首先检查用户名和邮箱的唯一性,然后使用`pkg/auth`包对密码进行加密,创建用户记录,并生成JWT Token。它还实现了头像URL的逻辑:优先使用用户提供的URL,否则从系统配置中获取默认头像。
|
||||
- `LoginUser`:处理用户登录。支持通过用户名或邮箱登录。方法会根据输入是否包含`@`符号来判断登录方式。成功登录后,会更新用户的最后登录时间,并记录登录日志(成功或失败)。
|
||||
- `ChangeUserPassword` 和 `ResetUserPassword`:分别处理用户主动修改密码和通过邮箱重置密码的场景。两者都包含密码验证和加密更新的逻辑。
|
||||
- `UpdateUserInfo` 和 `UpdateUserAvatar`:用于更新用户的基本信息和头像。
|
||||
|
||||
该服务通过调用`repository.FindUserByUsername`、`repository.FindUserByEmail`等方法与Repository层交互,确保了数据访问的抽象化。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class UserService {
|
||||
+RegisterUser(jwtService *auth.JWTService, username, password, email, avatar string) (*model.User, string, error)
|
||||
+LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error)
|
||||
+GetUserByID(id int64) (*model.User, error)
|
||||
+UpdateUserInfo(user *model.User) error
|
||||
+UpdateUserAvatar(userID int64, avatarURL string) error
|
||||
+ChangeUserPassword(userID int64, oldPassword, newPassword string) error
|
||||
+ResetUserPassword(email, newPassword string) error
|
||||
+ChangeUserEmail(userID int64, newEmail string) error
|
||||
}
|
||||
class User {
|
||||
+ID int64
|
||||
+Username string
|
||||
+Email string
|
||||
+Avatar string
|
||||
+Points int
|
||||
+Role string
|
||||
+Status int16
|
||||
}
|
||||
class Repository {
|
||||
+FindUserByUsername(username string) (*model.User, error)
|
||||
+FindUserByEmail(email string) (*model.User, error)
|
||||
+CreateUser(user *model.User) error
|
||||
+UpdateUserFields(id int64, fields map[string]interface{}) error
|
||||
}
|
||||
class Auth {
|
||||
+HashPassword(password string) (string, error)
|
||||
+CheckPassword(hashedPassword, password string) bool
|
||||
+GenerateToken(userID int64, username, role string) (string, error)
|
||||
}
|
||||
UserService --> User : "操作"
|
||||
UserService --> Repository : "调用"
|
||||
UserService --> Auth : "依赖"
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L249)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
|
||||
**本节来源**
|
||||
- [user_service.go](file://internal/service/user_service.go#L12-L249)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
|
||||
### ProfileService 分析
|
||||
|
||||
`ProfileService`负责管理用户的Minecraft档案(Profile),包括创建、查询、更新、删除和状态管理。
|
||||
|
||||
**核心方法与职责**:
|
||||
- `CreateProfile`:创建新的Minecraft档案。此方法首先验证用户存在且状态正常,检查角色名的唯一性,然后生成一个UUID作为档案ID,并调用`generateRSAPrivateKey`生成RSA-2048私钥(PEM格式)。创建档案后,会调用`SetActiveProfile`确保该档案成为用户的活跃档案。
|
||||
- `GetUserProfiles` 和 `GetProfileByUUID`:分别用于获取用户的所有档案列表和单个档案的详细信息。
|
||||
- `UpdateProfile`:更新档案信息,如角色名、皮肤和披风。更新角色名时会检查是否重复。更新后会重新加载数据以返回最新状态。
|
||||
- `DeleteProfile`:删除指定的档案。
|
||||
- `SetActiveProfile`:将指定档案设置为活跃状态。这是一个关键的业务方法,它在一个数据库事务中执行:首先将用户的所有档案设置为非活跃,然后将指定的档案设置为活跃,从而保证了同一时间只有一个活跃档案。
|
||||
- `CheckProfileLimit`:检查用户当前的档案数量是否达到了系统设定的上限,用于在创建新档案前进行限制。
|
||||
|
||||
该服务通过`repository.FindProfileByUUID`、`repository.CreateProfile`等方法与Repository层交互,并利用GORM的事务功能来保证`SetActiveProfile`操作的原子性。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ProfileService {
|
||||
+CreateProfile(db *gorm.DB, userID int64, name string) (*model.Profile, error)
|
||||
+GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error)
|
||||
+GetUserProfiles(db *gorm.DB, userID int64) ([]*model.Profile, error)
|
||||
+UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error)
|
||||
+DeleteProfile(db *gorm.DB, uuid string, userID int64) error
|
||||
+SetActiveProfile(db *gorm.DB, uuid string, userID int64) error
|
||||
+CheckProfileLimit(db *gorm.DB, userID int64, maxProfiles int) error
|
||||
}
|
||||
class Profile {
|
||||
+UUID string
|
||||
+UserID int64
|
||||
+Name string
|
||||
+SkinID *int64
|
||||
+CapeID *int64
|
||||
+RSAPrivateKey string
|
||||
+IsActive bool
|
||||
}
|
||||
class Repository {
|
||||
+FindProfileByUUID(uuid string) (*model.Profile, error)
|
||||
+CreateProfile(profile *model.Profile) error
|
||||
+FindProfilesByUserID(userID int64) ([]*model.Profile, error)
|
||||
+CountProfilesByUserID(userID int64) (int64, error)
|
||||
+SetActiveProfile(uuid string, userID int64) error
|
||||
+DeleteProfile(uuid string) error
|
||||
}
|
||||
ProfileService --> Profile : "操作"
|
||||
ProfileService --> Repository : "调用"
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
**本节来源**
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
|
||||
### TextureService 分析
|
||||
|
||||
`TextureService`负责管理用户上传的皮肤(Skin)和披风(Cape)材质。
|
||||
|
||||
**核心方法与职责**:
|
||||
- `CreateTexture`:创建新的材质记录。方法会验证上传者用户的存在性,并检查材质的Hash值是否已存在(防止重复上传)。它通过`switch`语句将字符串类型的`textureType`转换为`model.TextureType`枚举值。创建成功后,材质记录会被持久化。
|
||||
- `SearchTextures` 和 `GetUserTextures`:提供按关键词、类型、公开性等条件搜索材质,以及获取特定用户上传的材质列表的功能。
|
||||
- `UpdateTexture`:允许上传者更新材质的名称、描述和公开状态。更新时会检查权限,确保只有上传者才能修改。
|
||||
- `DeleteTexture`:允许上传者删除自己的材质。
|
||||
- `RecordTextureDownload`:记录一次材质下载行为。该方法会增加材质的下载计数,并创建一条下载日志。
|
||||
- `ToggleTextureFavorite`:切换用户对某个材质的收藏状态。如果已收藏则取消收藏并减少收藏计数,反之则添加收藏并增加收藏计数。
|
||||
- `CheckTextureUploadLimit`:检查用户上传的材质数量是否达到了系统设定的上限。
|
||||
|
||||
该服务通过`repository.FindTextureByHash`、`repository.CreateTexture`等方法与Repository层交互,并通过`IncrementTextureDownloadCount`和`DecrementTextureFavoriteCount`等原子操作来安全地更新计数。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class TextureService {
|
||||
+CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error)
|
||||
+GetTextureByID(db *gorm.DB, id int64) (*model.Texture, error)
|
||||
+GetUserTextures(db *gorm.DB, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
+SearchTextures(db *gorm.DB, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
+UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error)
|
||||
+DeleteTexture(db *gorm.DB, textureID, uploaderID int64) error
|
||||
+RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddress, userAgent string) error
|
||||
+ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error)
|
||||
+GetUserTextureFavorites(db *gorm.DB, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
+CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) error
|
||||
}
|
||||
class Texture {
|
||||
+ID int64
|
||||
+UploaderID int64
|
||||
+Name string
|
||||
+Type TextureType
|
||||
+URL string
|
||||
+Hash string
|
||||
+DownloadCount int
|
||||
+FavoriteCount int
|
||||
}
|
||||
class Repository {
|
||||
+FindTextureByHash(hash string) (*model.Texture, error)
|
||||
+CreateTexture(texture *model.Texture) error
|
||||
+FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
+SearchTextures(keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
+UpdateTextureFields(id int64, fields map[string]interface{}) error
|
||||
+DeleteTexture(id int64) error
|
||||
+IncrementTextureDownloadCount(id int64) error
|
||||
+IncrementTextureFavoriteCount(id int64) error
|
||||
+DecrementTextureFavoriteCount(id int64) error
|
||||
+IsTextureFavorited(userID, textureID int64) (bool, error)
|
||||
}
|
||||
TextureService --> Texture : "操作"
|
||||
TextureService --> Repository : "调用"
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
**本节来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
|
||||
## 服务调用关系与流程
|
||||
|
||||
服务层的调用流程通常始于`handler`层的HTTP请求处理器。例如,当用户发起注册请求时,`auth_handler.go`中的处理器会调用`UserService.RegisterUser`方法。该方法内部会协调多个Repository操作(检查用户、创建用户),并依赖`pkg/auth`包进行密码加密和Token生成。
|
||||
|
||||
服务之间通常是独立的,但它们都依赖于`Repository`层来访问数据。例如,`ProfileService.CreateProfile`在创建档案时,会先调用`repository.FindUserByID`来验证用户,这与`UserService`使用的Repository是同一个,体现了数据访问层的共享性。
|
||||
|
||||
一个典型的跨服务流程是用户上传材质并将其设置为档案皮肤:
|
||||
1. `TextureService.CreateTexture` 被调用,创建材质记录。
|
||||
2. `ProfileService.UpdateProfile` 被调用,传入新创建的材质ID作为`skinID`参数,更新档案的皮肤引用。
|
||||
|
||||
这种设计保证了服务的单一职责,同时通过Repository层实现了数据的统一管理。
|
||||
|
||||
## 业务规则实现
|
||||
|
||||
服务层是复杂业务规则的主要实现者。
|
||||
|
||||
**档案数量限制**:
|
||||
`ProfileService.CheckProfileLimit`方法通过调用`repository.CountProfilesByUserID`获取用户当前的档案数量,并与传入的`maxProfiles`上限进行比较。这个检查通常在`CreateProfile`方法的开头执行,以防止用户创建过多的档案。
|
||||
|
||||
**材质上传流程**:
|
||||
`TextureService.CreateTexture`方法实现了严格的上传流程:
|
||||
1. **用户验证**:确保上传者是有效的用户。
|
||||
2. **去重检查**:通过`repository.FindTextureByHash`检查材质的Hash值,避免存储重复内容。
|
||||
3. **类型验证**:使用`switch`语句确保`textureType`是有效的("SKIN"或"CAPE")。
|
||||
4. **数据创建**:构建`model.Texture`对象并调用`repository.CreateTexture`进行持久化。
|
||||
|
||||
**活跃档案管理**:
|
||||
`ProfileService.SetActiveProfile`是实现“一个用户只能有一个活跃档案”这一业务规则的核心。它利用GORM的事务功能,在一个原子操作中完成两步:
|
||||
1. 将用户所有档案的`is_active`字段更新为`false`。
|
||||
2. 将指定档案的`is_active`字段更新为`true`。
|
||||
这确保了即使在高并发场景下,也不会出现多个档案同时为活跃状态的情况。
|
||||
|
||||
## 错误处理与日志记录
|
||||
|
||||
服务层实现了细粒度的错误处理。每个方法都会返回一个`error`类型的值,用于向调用者传递错误信息。错误通常使用`fmt.Errorf`进行包装,以提供上下文信息,例如`fmt.Errorf("查询用户失败: %w", err)`。
|
||||
|
||||
此外,服务层还直接负责记录关键操作日志:
|
||||
- `UserService`中的`logSuccessLogin`和`logFailedLogin`方法会创建`UserLoginLog`记录,用于审计登录行为。
|
||||
- `TextureService`中的`RecordTextureDownload`方法会创建`TextureDownloadLog`记录,用于追踪下载量。
|
||||
|
||||
这些日志记录通过调用相应的Repository方法(如`repository.CreateLoginLog`)来持久化,为系统监控和问题排查提供了重要依据。
|
||||
|
||||
**本节来源**
|
||||
- [user_service.go](file://internal/service/user_service.go#L203-L226)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L162-L187)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go#L77-L81)
|
||||
- [texture_repository.go](file://internal/repository/texture_repository.go#L153-L157)
|
||||
|
||||
## 结论
|
||||
|
||||
CarrotSkin项目的服务层设计清晰、职责分明,有效地封装了核心业务逻辑。`UserService`、`ProfileService`和`TextureService`三个核心服务各司其职,通过调用统一的Repository层来访问数据,实现了业务逻辑与数据持久化的解耦。服务层成功实现了档案数量限制、材质上传去重、活跃档案管理等复杂业务规则,并通过事务和原子操作保证了数据的一致性。其良好的错误处理和日志记录机制为系统的稳定运行和维护提供了保障。该架构为未来的功能扩展(如积分系统、审核流程)奠定了坚实的基础。
|
||||
@@ -1,462 +0,0 @@
|
||||
# 材质服务
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go)
|
||||
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者,系统性梳理材质服务(TextureService)的职责、方法与内部逻辑,重点覆盖:
|
||||
- 材质类型(SKIN/CAPE)校验与默认值设定(状态、下载数、收藏数)
|
||||
- 材质状态管理(正常、禁用、删除)
|
||||
- 上传、搜索、收藏等业务规则及服务层与仓储层的协作
|
||||
- 状态验证逻辑(状态为-1表示已删除,无效)
|
||||
- 常见问题与错误处理策略
|
||||
- 性能优化建议(如高效查询热门材质)
|
||||
|
||||
## 项目结构
|
||||
围绕材质服务的关键文件组织如下:
|
||||
- 服务层:负责业务规则编排与跨仓储调用
|
||||
- 仓储层:封装数据库访问与聚合查询
|
||||
- 模型层:定义材质实体、收藏关联与下载日志
|
||||
- 数据库管理:统一数据库连接与自动迁移
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "服务层"
|
||||
S1["TextureService<br/>internal/service/texture_service.go"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
R1["TextureRepository<br/>internal/repository/texture_repository.go"]
|
||||
end
|
||||
subgraph "模型层"
|
||||
M1["Texture<br/>internal/model/texture.go"]
|
||||
M2["UserTextureFavorite<br/>internal/model/texture.go"]
|
||||
M3["TextureDownloadLog<br/>internal/model/texture.go"]
|
||||
end
|
||||
subgraph "基础设施"
|
||||
D1["数据库管理器<br/>pkg/database/manager.go"]
|
||||
end
|
||||
S1 --> R1
|
||||
R1 --> D1
|
||||
S1 --> M1
|
||||
S1 --> M2
|
||||
S1 --> M3
|
||||
R1 --> M1
|
||||
R1 --> M2
|
||||
R1 --> M3
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
|
||||
## 核心组件
|
||||
- TextureService:提供材质上传、查询、更新、删除、下载统计、收藏切换、收藏列表、上传数量限制检查等能力
|
||||
- TextureRepository:封装材质的增删改查、分页、统计、收藏与下载日志写入等仓储操作
|
||||
- Model.Texture:材质实体,包含类型、状态、公开度、下载/收藏计数、Slim标记等
|
||||
- Model.UserTextureFavorite:用户-材质收藏关联
|
||||
- Model.TextureDownloadLog:材质下载日志
|
||||
- Database Manager:数据库连接与自动迁移
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
|
||||
## 架构总览
|
||||
服务层通过统一的 GORM 数据库实例访问仓储层,仓储层对数据库执行具体操作;模型层定义实体与索引约束,确保查询与统计效率。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "调用方"
|
||||
participant S as "TextureService"
|
||||
participant R as "TextureRepository"
|
||||
participant DB as "GORM DB"
|
||||
participant M as "Model"
|
||||
C->>S : 调用上传/搜索/收藏等方法
|
||||
S->>R : 调用仓储方法查询/更新/写入
|
||||
R->>DB : 执行数据库操作
|
||||
DB-->>R : 返回结果/影响行数
|
||||
R-->>S : 返回实体/列表/统计
|
||||
S-->>C : 返回业务结果
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### TextureService 方法与职责
|
||||
- 创建材质
|
||||
- 校验上传者存在性
|
||||
- 基于哈希去重
|
||||
- 类型转换与校验(仅支持 SKIN/CAPE)
|
||||
- 默认状态为“正常”,默认下载数与收藏数为 0
|
||||
- 写入仓储并返回实体
|
||||
- 获取材质详情
|
||||
- 查询材质并校验状态(状态为-1视为已删除)
|
||||
- 用户上传列表
|
||||
- 分页查询,排除已删除状态
|
||||
- 搜索材质
|
||||
- 支持公开筛选、类型筛选、关键词模糊匹配
|
||||
- 分页查询,按创建时间倒序
|
||||
- 更新材质
|
||||
- 权限校验(仅上传者可更新)
|
||||
- 动态更新名称/描述/公开状态
|
||||
- 删除材质
|
||||
- 权限校验(仅上传者可删除)
|
||||
- 采用软删除(状态置为-1)
|
||||
- 记录下载
|
||||
- 校验材质存在性
|
||||
- 原子递增下载计数
|
||||
- 写入下载日志
|
||||
- 切换收藏
|
||||
- 校验材质存在性
|
||||
- 已收藏则取消并递减收藏计数;未收藏则添加并递增收藏计数
|
||||
- 用户收藏列表
|
||||
- 分页查询收藏的材质(排除已删除)
|
||||
- 上传数量限制检查
|
||||
- 统计用户未删除材质数量并与上限比较
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
|
||||
### 材质类型与默认值
|
||||
- 类型枚举
|
||||
- SKIN、CAPE 两种类型
|
||||
- 默认值
|
||||
- 状态:1(正常)
|
||||
- 下载数:0
|
||||
- 收藏数:0
|
||||
- Slim 标记:用于区分 Alex/Steve 角色皮肤样式
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
|
||||
### 状态管理与验证
|
||||
- 状态含义
|
||||
- 1:正常
|
||||
- 0:审核中/禁用(仍存在,但不可用)
|
||||
- -1:已删除(软删除)
|
||||
- 验证规则
|
||||
- 获取详情时若状态为-1,判定为“已删除”
|
||||
- 搜索与收藏列表均排除状态为-1的材质
|
||||
- 删除操作采用软删除,不物理删除
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
|
||||
### 上传、搜索与收藏业务规则
|
||||
- 上传
|
||||
- 哈希唯一性约束,避免重复
|
||||
- 类型严格校验
|
||||
- 默认状态与计数初始化
|
||||
- 搜索
|
||||
- 公开筛选、类型筛选、关键词模糊匹配
|
||||
- 分页与排序(按创建时间倒序)
|
||||
- 收藏
|
||||
- 基于用户-材质关联表维护收藏
|
||||
- 切换收藏时原子更新计数
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
### 服务层与仓储层协作
|
||||
- 服务层负责:
|
||||
- 参数校验与业务规则
|
||||
- 权限校验(上传者)
|
||||
- 组合仓储调用以完成复杂流程(如收藏切换)
|
||||
- 仓储层负责:
|
||||
- 数据库 CRUD、分页、统计、索引使用
|
||||
- 原子计数更新(下载/收藏)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class TextureService {
|
||||
+CreateTexture(...)
|
||||
+GetTextureByID(...)
|
||||
+GetUserTextures(...)
|
||||
+SearchTextures(...)
|
||||
+UpdateTexture(...)
|
||||
+DeleteTexture(...)
|
||||
+RecordTextureDownload(...)
|
||||
+ToggleTextureFavorite(...)
|
||||
+GetUserTextureFavorites(...)
|
||||
+CheckTextureUploadLimit(...)
|
||||
}
|
||||
class TextureRepository {
|
||||
+CreateTexture(...)
|
||||
+FindTextureByID(...)
|
||||
+FindTextureByHash(...)
|
||||
+FindTexturesByUploaderID(...)
|
||||
+SearchTextures(...)
|
||||
+UpdateTexture(...)
|
||||
+UpdateTextureFields(...)
|
||||
+DeleteTexture(...)
|
||||
+IncrementTextureDownloadCount(...)
|
||||
+IncrementTextureFavoriteCount(...)
|
||||
+DecrementTextureFavoriteCount(...)
|
||||
+CreateTextureDownloadLog(...)
|
||||
+IsTextureFavorited(...)
|
||||
+AddTextureFavorite(...)
|
||||
+RemoveTextureFavorite(...)
|
||||
+GetUserTextureFavorites(...)
|
||||
+CountTexturesByUploaderID(...)
|
||||
}
|
||||
class Texture
|
||||
class UserTextureFavorite
|
||||
class TextureDownloadLog
|
||||
TextureService --> TextureRepository : "调用"
|
||||
TextureRepository --> Texture : "读写"
|
||||
TextureRepository --> UserTextureFavorite : "读写"
|
||||
TextureRepository --> TextureDownloadLog : "读写"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
### 关键流程图示
|
||||
|
||||
#### 获取材质详情流程
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入 GetTextureByID"]) --> Load["仓储查询材质"]
|
||||
Load --> Found{"找到记录?"}
|
||||
Found --> |否| NotFound["返回不存在错误"]
|
||||
Found --> |是| CheckStatus["检查状态"]
|
||||
CheckStatus --> Deleted{"状态为-1?"}
|
||||
Deleted --> |是| DelErr["返回已删除错误"]
|
||||
Deleted --> |否| Return["返回材质对象"]
|
||||
NotFound --> End(["结束"])
|
||||
DelErr --> End
|
||||
Return --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L16-L27)
|
||||
|
||||
#### 切换收藏流程
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入 ToggleTextureFavorite"]) --> Load["仓储查询材质"]
|
||||
Load --> Found{"找到记录?"}
|
||||
Found --> |否| Err["返回不存在错误"]
|
||||
Found --> |是| CheckFav["检查是否已收藏"]
|
||||
CheckFav --> IsFav{"已收藏?"}
|
||||
IsFav --> |是| Unfav["删除收藏记录"]
|
||||
Unfav --> Dec["递减收藏计数"]
|
||||
IsFav --> |否| Fav["新增收藏记录"]
|
||||
Fav --> Inc["递增收藏计数"]
|
||||
Dec --> Done["返回false"]
|
||||
Inc --> Done2["返回true"]
|
||||
Err --> End(["结束"])
|
||||
Done --> End
|
||||
Done2 --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
|
||||
|
||||
#### 上传流程(含类型与去重校验)
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入 CreateTexture"]) --> CheckUser["校验上传者存在"]
|
||||
CheckUser --> Exists{"存在?"}
|
||||
Exists --> |否| UErr["返回用户不存在错误"]
|
||||
Exists --> |是| CheckHash["按哈希查询材质"]
|
||||
CheckHash --> Dup{"已存在?"}
|
||||
Dup --> |是| DupErr["返回材质已存在错误"]
|
||||
Dup --> |否| ParseType["解析类型(SKIN/CAPE)"]
|
||||
ParseType --> Valid{"类型有效?"}
|
||||
Valid --> |否| TErr["返回无效类型错误"]
|
||||
Valid --> |是| Build["构建材质对象(默认状态/计数)"]
|
||||
Build --> Save["仓储保存材质"]
|
||||
Save --> Ok["返回材质对象"]
|
||||
UErr --> End(["结束"])
|
||||
DupErr --> End
|
||||
TErr --> End
|
||||
Ok --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L29-L41)
|
||||
|
||||
## 依赖关系分析
|
||||
- 服务层依赖仓储层接口,仓储层依赖数据库管理器提供的全局 GORM 实例
|
||||
- 模型层定义了材质、收藏、下载日志三类实体,并带有索引与排序提示
|
||||
- 数据库管理器负责初始化与迁移,确保表结构与模型一致
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
TS["TextureService"] --> TR["TextureRepository"]
|
||||
TR --> DM["Database Manager"]
|
||||
TR --> M["Models(Texture/UserTextureFavorite/TextureDownloadLog)"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L1-L114)
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
## 性能考虑
|
||||
- 索引与排序
|
||||
- 材质表对公开状态、类型、状态组合建立索引,有利于搜索过滤
|
||||
- 下载数与收藏数列带索引且按降序排序,便于热门查询
|
||||
- 原子计数更新
|
||||
- 下载与收藏计数采用原生表达式递增/递减,减少锁竞争与往返
|
||||
- 分页与预加载
|
||||
- 分页查询时使用偏移与限制,结合预加载提升列表渲染效率
|
||||
- 热门查询建议
|
||||
- 使用下载数/收藏数列的降序索引进行 Top-N 查询
|
||||
- 对搜索场景,优先利用公开状态与类型过滤,缩小扫描范围
|
||||
- 缓存策略(建议)
|
||||
- 对热点材质详情与热门榜单可引入缓存层,降低数据库压力
|
||||
- 结合 TTL 与失效策略,保证数据一致性
|
||||
|
||||
章节来源
|
||||
- [internal/model/texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L93-L103)
|
||||
|
||||
## 故障排查指南
|
||||
- 无效类型
|
||||
- 现象:上传时返回“无效的材质类型”
|
||||
- 排查:确认传入类型为 SKIN 或 CAPE
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L32-L41)
|
||||
- 材质已存在
|
||||
- 现象:上传返回“该材质已存在”
|
||||
- 排查:确认哈希是否重复;检查去重逻辑
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31)
|
||||
- 用户不存在
|
||||
- 现象:上传返回“用户不存在”
|
||||
- 排查:确认上传者ID正确;检查用户是否存在
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L14-L21)
|
||||
- 材质不存在或已删除
|
||||
- 现象:获取详情/收藏/下载时返回“材质不存在”或“材质已删除”
|
||||
- 排查:确认ID正确;检查状态是否为-1
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
|
||||
- 权限不足
|
||||
- 现象:更新/删除返回“无权修改/无权删除”
|
||||
- 排查:确认请求者为上传者
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
- 上传数量超限
|
||||
- 现象:检查上传限制返回已达上限
|
||||
- 排查:确认当前未删除材质数量与上限配置
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
|
||||
|
||||
章节来源
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
|
||||
|
||||
## 结论
|
||||
TextureService 将业务规则与仓储操作解耦,通过严格的类型与状态校验、完善的权限控制与原子计数更新,保障了材质上传、搜索、收藏与统计的可靠性。配合模型层的索引设计与服务层的分页策略,可在高并发场景下保持良好性能。建议在热点场景引入缓存与更精细的索引策略,持续优化查询与写入性能。
|
||||
|
||||
## 附录
|
||||
|
||||
### API 一览(方法与关键行为)
|
||||
- CreateTexture
|
||||
- 输入:上传者ID、名称、描述、类型、URL、哈希、大小、公开/Slim、默认状态与计数
|
||||
- 行为:校验用户与哈希唯一性,类型转换,创建并返回
|
||||
- 错误:用户不存在、哈希重复、无效类型
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
|
||||
- GetTextureByID
|
||||
- 输入:材质ID
|
||||
- 行为:查询并校验状态(-1视为已删除)
|
||||
- 错误:不存在、已删除
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
|
||||
- GetUserTextures
|
||||
- 输入:上传者ID、分页参数
|
||||
- 行为:分页查询用户上传材质(排除已删除)
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L81-L91)
|
||||
- SearchTextures
|
||||
- 输入:关键词、类型、公开筛选、分页参数
|
||||
- 行为:按条件过滤并分页,按创建时间倒序
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L93-L103)
|
||||
- UpdateTexture
|
||||
- 输入:材质ID、上传者ID、名称/描述/公开状态
|
||||
- 行为:权限校验后动态更新字段
|
||||
- 错误:不存在、权限不足
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L106-L141)
|
||||
- DeleteTexture
|
||||
- 输入:材质ID、上传者ID
|
||||
- 行为:权限校验后软删除(状态=-1)
|
||||
- 错误:不存在、权限不足
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L144-L160)
|
||||
- RecordTextureDownload
|
||||
- 输入:材质ID、用户ID/IP/UA
|
||||
- 行为:原子递增下载计数并写入日志
|
||||
- 错误:不存在
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187)
|
||||
- ToggleTextureFavorite
|
||||
- 输入:用户ID、材质ID
|
||||
- 行为:切换收藏并原子更新计数
|
||||
- 错误:不存在
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225)
|
||||
- GetUserTextureFavorites
|
||||
- 输入:用户ID、分页参数
|
||||
- 行为:分页查询收藏的材质(排除已删除)
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L227-L237)
|
||||
- CheckTextureUploadLimit
|
||||
- 输入:上传者ID、最大数量
|
||||
- 行为:统计未删除材质数量并与上限比较
|
||||
- 错误:达到上限
|
||||
- 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
|
||||
|
||||
### 状态与类型验证(测试参考)
|
||||
- 类型验证:仅接受 SKIN/CAPE
|
||||
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L7-L44)
|
||||
- 默认值验证:状态=1,下载数=0,收藏数=0
|
||||
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L46-L65)
|
||||
- 状态验证:状态=-1 无效
|
||||
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L67-L99)
|
||||
- 分页参数校验:page≥1,1≤pageSize≤100
|
||||
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L102-L161)
|
||||
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
|
||||
- 参考路径:[internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)
|
||||
@@ -1,365 +0,0 @@
|
||||
# 档案服务
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [profile_service.go](file://internal/service/profile_service.go)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go)
|
||||
- [profile.go](file://internal/model/profile.go)
|
||||
- [user.go](file://internal/model/user.go)
|
||||
- [texture.go](file://internal/model/texture.go)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go)
|
||||
- [common.go](file://internal/types/common.go)
|
||||
- [profile_service_test.go](file://internal/service/profile_service_test.go)
|
||||
- [profile_handler_test.go](file://internal/handler/profile_handler_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本档“档案服务”文档聚焦于 ProfileService 的职责、方法与内部逻辑,覆盖档案的创建、更新、删除与活跃状态管理;明确档案名称与用户ID的验证规则(名称非空且长度1-16,用户ID大于0),以及状态有效性判断(仅当状态为1时表示正常可用);阐述新创建档案默认为活跃状态的业务规则,并说明多档案用户的活跃档案切换逻辑;解释档案与用户、材质之间的关联关系;最后提供常见错误场景的排查指南,帮助开发者快速定位问题。
|
||||
|
||||
## 项目结构
|
||||
档案服务位于 internal/service 层,通过 internal/handler 接收HTTP请求,调用 ProfileService 完成业务逻辑,再由 ProfileService 调用 internal/repository 访问数据库;数据模型定义在 internal/model 中,类型定义在 internal/types 中。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "接口层"
|
||||
H["profile_handler.go<br/>HTTP处理器"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["profile_service.go<br/>ProfileService"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
R["profile_repository.go<br/>Profile仓储"]
|
||||
end
|
||||
subgraph "模型层"
|
||||
M1["profile.go<br/>Profile模型"]
|
||||
M2["user.go<br/>User模型"]
|
||||
M3["texture.go<br/>Texture模型"]
|
||||
end
|
||||
subgraph "类型定义"
|
||||
T["common.go<br/>CreateProfileRequest/UpdateProfileRequest/ProfileInfo"]
|
||||
end
|
||||
H --> S
|
||||
S --> R
|
||||
R --> M1
|
||||
R --> M2
|
||||
R --> M3
|
||||
H --> T
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [common.go](file://internal/types/common.go#L81-L207)
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [common.go](file://internal/types/common.go#L81-L207)
|
||||
|
||||
## 核心组件
|
||||
- ProfileService:负责档案的创建、查询、更新、删除、活跃状态设置与数量限制检查等核心业务逻辑。
|
||||
- ProfileRepository:封装数据库访问,包括创建、查询、更新、删除、统计、设置活跃状态等。
|
||||
- Profile 模型:定义档案的数据结构及与用户、材质的关联。
|
||||
- Handler:接收HTTP请求,解析参数,调用服务层并返回统一响应。
|
||||
- 类型定义:CreateProfileRequest、UpdateProfileRequest、ProfileInfo 等用于API契约与响应结构。
|
||||
|
||||
章节来源
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
- [common.go](file://internal/types/common.go#L81-L207)
|
||||
|
||||
## 架构总览
|
||||
档案服务采用典型的三层架构:Handler 负责接口与参数校验,Service 负责业务规则与流程编排,Repository 负责数据持久化。ProfileService 在创建档案时会进行用户存在性与状态校验、角色名唯一性校验、生成UUID与RSA密钥、创建档案并将其设置为活跃状态;在设置活跃状态时,通过事务将该用户下的其他档案全部置为非活跃,确保同一时刻仅有一个活跃档案。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "Handler"
|
||||
participant S as "ProfileService"
|
||||
participant R as "ProfileRepository"
|
||||
participant DB as "数据库"
|
||||
C->>H : "POST /api/v1/profile"
|
||||
H->>H : "解析请求体(CreateProfileRequest)"
|
||||
H->>S : "CheckProfileLimit(userID, maxProfiles)"
|
||||
S->>R : "CountProfilesByUserID(userID)"
|
||||
R-->>S : "count"
|
||||
S-->>H : "通过/错误"
|
||||
H->>S : "CreateProfile(userID, name)"
|
||||
S->>R : "FindUserByID(userID)"
|
||||
R-->>S : "User"
|
||||
S->>R : "FindProfileByName(name)"
|
||||
R-->>S : "Profile 或 NotFound"
|
||||
S->>S : "生成UUID与RSA私钥"
|
||||
S->>R : "CreateProfile(Profile)"
|
||||
R->>DB : "INSERT"
|
||||
DB-->>R : "OK"
|
||||
S->>R : "SetActiveProfile(uuid, userID)"
|
||||
R->>DB : "事务 : 先将用户其他档案置为非活跃,再将目标置为活跃"
|
||||
DB-->>R : "OK"
|
||||
R-->>S : "OK"
|
||||
S-->>H : "Profile"
|
||||
H-->>C : "200 成功响应"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L93)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L68)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L109)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### ProfileService 方法与职责
|
||||
- 创建档案
|
||||
- 输入:用户ID、档案名称
|
||||
- 校验:用户存在且状态为1;档案名称唯一;名称长度1-16
|
||||
- 生成:UUID、RSA私钥(PEM格式)
|
||||
- 写入:创建档案记录,默认 IsActive=true
|
||||
- 并发安全:通过事务将该用户下其他档案置为非活跃,确保仅一个活跃档案
|
||||
- 获取档案详情
|
||||
- 输入:档案UUID
|
||||
- 输出:Profile(预加载Skin与Cape)
|
||||
- 获取用户档案列表
|
||||
- 输入:用户ID
|
||||
- 输出:Profile 列表(按创建时间倒序)
|
||||
- 更新档案
|
||||
- 输入:UUID、用户ID、可选名称、可选SkinID、可选CapeID
|
||||
- 校验:权限(档案属于当前用户)、名称唯一性(若更改)
|
||||
- 更新:保存变更并重新加载以返回最新数据
|
||||
- 删除档案
|
||||
- 输入:UUID、用户ID
|
||||
- 校验:权限
|
||||
- 删除:物理删除档案
|
||||
- 设置活跃档案
|
||||
- 输入:UUID、用户ID
|
||||
- 校验:权限
|
||||
- 事务:将该用户下其他档案置为非活跃,再将目标置为活跃
|
||||
- 同步:更新最后使用时间
|
||||
- 档案数量限制
|
||||
- 输入:用户ID、最大数量
|
||||
- 校验:统计当前档案数是否达到上限
|
||||
- 校验档案归属
|
||||
- 输入:用户ID、档案UUID
|
||||
- 校验:UUID存在且属于该用户
|
||||
- 名称批量查询
|
||||
- 输入:名称数组
|
||||
- 输出:Profile 列表
|
||||
- 密钥对查询
|
||||
- 输入:档案ID
|
||||
- 输出:KeyPair(私钥、公钥、过期时间等)
|
||||
|
||||
章节来源
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L177)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
### 数据模型与关联关系
|
||||
- Profile
|
||||
- 字段:UUID、UserID、Name、SkinID、CapeID、RSAPrivateKey、IsActive、LastUsedAt、CreatedAt、UpdatedAt
|
||||
- 关联:User(外键 UserID)、Texture(SkinID、CapeID)
|
||||
- User
|
||||
- 字段:ID、Username、Email、Role、Status(1: 正常, 0: 禁用, -1: 删除)
|
||||
- Texture
|
||||
- 字段:ID、UploaderID、Name、Type、URL、Hash、IsPublic、DownloadCount、FavoriteCount、IsSlim、Status(1: 正常, 0: 审核中, -1: 已删除)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER {
|
||||
bigint id PK
|
||||
varchar username UK
|
||||
varchar email UK
|
||||
smallint status
|
||||
}
|
||||
PROFILE {
|
||||
varchar uuid PK
|
||||
bigint user_id FK
|
||||
varchar name UK
|
||||
bigint skin_id
|
||||
bigint cape_id
|
||||
boolean is_active
|
||||
timestamp last_used_at
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
TEXTURE {
|
||||
bigint id PK
|
||||
bigint uploader_id FK
|
||||
varchar name
|
||||
varchar type
|
||||
varchar url
|
||||
varchar hash UK
|
||||
boolean is_public
|
||||
integer download_count
|
||||
integer favorite_count
|
||||
boolean is_slim
|
||||
smallint status
|
||||
}
|
||||
USER ||--o{ PROFILE : "拥有"
|
||||
TEXTURE ||--o{ PROFILE : "被使用(Skin/Cape)"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
### 验证规则与状态判断
|
||||
- 档案名称
|
||||
- 非空且长度在1-16之间
|
||||
- 更新时仅当名称发生变更才检查唯一性
|
||||
- 用户ID
|
||||
- 必须大于0
|
||||
- 用户状态
|
||||
- 仅当用户状态为1(正常)时允许创建档案
|
||||
- 活跃状态
|
||||
- 默认 IsActive=true
|
||||
- 仅当状态为1时表示“正常可用”
|
||||
- 设置活跃状态时,通过事务将该用户下其他档案置为非活跃
|
||||
|
||||
章节来源
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L68)
|
||||
- [profile_service_test.go](file://internal/service/profile_service_test.go#L1-L77)
|
||||
- [common.go](file://internal/types/common.go#L81-L207)
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
|
||||
### 活跃档案切换逻辑
|
||||
- 设计目标:同一用户在同一时刻仅有一个活跃档案
|
||||
- 实现方式:在设置活跃档案时,使用数据库事务
|
||||
- 先将该用户下所有档案的 IsActive=false
|
||||
- 再将目标档案 IsActive=true
|
||||
- 最后更新最后使用时间
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> Load["加载档案(uuid, userID)"]
|
||||
Load --> CheckPerm{"权限校验通过?"}
|
||||
CheckPerm --> |否| ErrPerm["返回无权操作"]
|
||||
CheckPerm --> |是| Txn["开启事务"]
|
||||
Txn --> SetAllFalse["将用户其他档案置为非活跃"]
|
||||
SetAllFalse --> SetTargetTrue["将目标档案置为活跃"]
|
||||
SetTargetTrue --> UpdateTime["更新最后使用时间"]
|
||||
UpdateTime --> Commit["提交事务"]
|
||||
Commit --> Done(["结束"])
|
||||
ErrPerm --> Done
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L161-L188)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L89-L109)
|
||||
|
||||
### API 与错误处理
|
||||
- 创建档案
|
||||
- 请求体:CreateProfileRequest(仅需 name,长度1-16)
|
||||
- 成功:返回 ProfileInfo(含 UUID、UserID、Name、IsActive、时间戳等)
|
||||
- 常见错误:未授权、参数错误、已达上限、服务器错误
|
||||
- 获取档案列表/详情
|
||||
- 成功:返回 ProfileInfo 列表/对象
|
||||
- 常见错误:未授权、服务器错误、档案不存在
|
||||
- 更新档案
|
||||
- 请求体:UpdateProfileRequest(可选 name、skin_id、cape_id)
|
||||
- 成功:返回更新后的 ProfileInfo
|
||||
- 常见错误:未授权、参数错误、无权操作、档案不存在、服务器错误
|
||||
- 删除档案
|
||||
- 成功:返回“删除成功”
|
||||
- 常见错误:未授权、无权操作、档案不存在、服务器错误
|
||||
- 设置活跃档案
|
||||
- 成功:返回“设置成功”
|
||||
- 常见错误:未授权、无权操作、档案不存在、服务器错误
|
||||
|
||||
章节来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L28-L399)
|
||||
- [common.go](file://internal/types/common.go#L81-L207)
|
||||
|
||||
## 依赖关系分析
|
||||
- Handler 依赖 Service
|
||||
- Service 依赖 Repository
|
||||
- Repository 依赖 Model 与数据库连接
|
||||
- Model 间通过外键建立关联
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
H["profile_handler.go"] --> S["profile_service.go"]
|
||||
S --> R["profile_repository.go"]
|
||||
R --> M1["profile.go"]
|
||||
R --> M2["user.go"]
|
||||
R --> M3["texture.go"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L1-L399)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
## 性能考量
|
||||
- 查询预加载:获取档案详情与列表时预加载 Skin 与 Cape,减少 N+1 查询风险
|
||||
- 事务一致性:设置活跃档案使用事务,保证原子性与一致性
|
||||
- 唯一索引:Name 与 Hash 等字段具备唯一索引,降低重复写入成本
|
||||
- 时间戳:LastUsedAt 便于后续统计与清理策略
|
||||
|
||||
[本节为通用建议,不涉及具体文件分析]
|
||||
|
||||
## 故障排查指南
|
||||
- 创建失败
|
||||
- 用户不存在或状态异常:检查用户是否存在且状态为1
|
||||
- 角色名重复:确认名称唯一性
|
||||
- 达到档案数量上限:检查当前用户档案数量与上限配置
|
||||
- 数据库错误:查看事务提交与唯一约束冲突
|
||||
- 更新失败
|
||||
- 无权操作:确认请求用户ID与档案所属用户一致
|
||||
- 名称重复:若修改了名称,需确保唯一性
|
||||
- 删除失败
|
||||
- 无权操作:确认请求用户ID与档案所属用户一致
|
||||
- 设置活跃失败
|
||||
- 无权操作:确认请求用户ID与档案所属用户一致
|
||||
- 事务回滚:检查数据库事务日志
|
||||
- 获取失败
|
||||
- 档案不存在:确认UUID正确且未被删除
|
||||
|
||||
章节来源
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L17-L253)
|
||||
- [profile_repository.go](file://internal/repository/profile_repository.go#L13-L177)
|
||||
- [profile_handler.go](file://internal/handler/profile_handler.go#L153-L399)
|
||||
|
||||
## 结论
|
||||
ProfileService 提供了完整的档案生命周期管理能力,涵盖创建、查询、更新、删除与活跃状态切换,并通过严格的验证规则与事务保障确保数据一致性。档案与用户、材质的关联清晰,便于扩展更多功能。建议在生产环境中结合日志与监控,持续优化性能与稳定性。
|
||||
|
||||
[本节为总结性内容,不涉及具体文件分析]
|
||||
|
||||
## 附录
|
||||
- 关键方法路径参考
|
||||
- 创建档案:[CreateProfile](file://internal/service/profile_service.go#L17-L68)
|
||||
- 获取档案详情:[GetProfileByUUID](file://internal/service/profile_service.go#L71-L81)
|
||||
- 获取用户档案列表:[GetUserProfiles](file://internal/service/profile_service.go#L83-L90)
|
||||
- 更新档案:[UpdateProfile](file://internal/service/profile_service.go#L92-L135)
|
||||
- 删除档案:[DeleteProfile](file://internal/service/profile_service.go#L137-L159)
|
||||
- 设置活跃档案:[SetActiveProfile](file://internal/service/profile_service.go#L161-L188)
|
||||
- 数量限制检查:[CheckProfileLimit](file://internal/service/profile_service.go#L190-L202)
|
||||
- 校验档案归属:[ValidateProfileByUserID](file://internal/service/profile_service.go#L222-L235)
|
||||
- 名称批量查询:[GetProfilesDataByNames](file://internal/service/profile_service.go#L237-L243)
|
||||
- 密钥对查询:[GetProfileKeyPair](file://internal/service/profile_service.go#L245-L253)
|
||||
- Handler 路由与错误映射
|
||||
- 创建/获取/更新/删除/设置活跃:[profile_handler.go](file://internal/handler/profile_handler.go#L28-L399)
|
||||
@@ -1,372 +0,0 @@
|
||||
# 用户服务
|
||||
|
||||
<cite>
|
||||
**本文引用的文件列表**
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [internal/model/user.go](file://internal/model/user.go)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go)
|
||||
- [internal/types/common.go](file://internal/types/common.go)
|
||||
- [internal/service/common.go](file://internal/service/common.go)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go)
|
||||
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件聚焦于用户服务(UserService)的职责、方法与内部逻辑,覆盖用户注册、登录、信息更新等核心功能,并对默认角色、状态与积分的初始化进行说明;同时解释头像选择策略(优先自定义头像,否则使用默认头像)以及数据验证规则(用户名、邮箱、密码非空且邮箱格式正确)。文档还提供服务层与仓储层的调用关系示例,帮助初学者理解业务流程,并为有经验的开发者提供扩展点建议(如修改默认角色、添加新的用户属性)。
|
||||
|
||||
## 项目结构
|
||||
用户服务位于 internal/service 层,围绕 internal/model 定义的数据模型工作,通过 internal/repository 与数据库交互,对外由 internal/handler 提供 HTTP 接口。认证相关的密码处理与 JWT 令牌生成分别由 pkg/auth 下的 password 与 jwt 组件完成。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "接口层"
|
||||
H["auth_handler<br/>HTTP接口"]
|
||||
end
|
||||
subgraph "服务层"
|
||||
S["user_service<br/>用户业务逻辑"]
|
||||
end
|
||||
subgraph "模型层"
|
||||
M["user.go<br/>User/UserLoginLog/UserPointLog"]
|
||||
SCM["system_config.go<br/>SystemConfig"]
|
||||
end
|
||||
subgraph "仓储层"
|
||||
R["user_repository<br/>CRUD/事务"]
|
||||
SCR["system_config_repository<br/>系统配置查询"]
|
||||
end
|
||||
subgraph "认证与工具"
|
||||
P["password.go<br/>密码加解密"]
|
||||
J["jwt.go<br/>JWT签发/校验"]
|
||||
T["types/common.go<br/>请求/响应结构"]
|
||||
end
|
||||
H --> S
|
||||
S --> R
|
||||
S --> SCR
|
||||
S --> P
|
||||
S --> J
|
||||
S --> M
|
||||
S --> SCM
|
||||
T --> H
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
|
||||
|
||||
章节来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
|
||||
## 核心组件
|
||||
- 用户模型(User):包含用户名、邮箱、头像、积分、角色、状态、最后登录时间等字段,以及唯一索引约束与默认值。
|
||||
- 用户登录日志(UserLoginLog):记录登录尝试的IP、UA、是否成功及失败原因。
|
||||
- 用户积分日志(UserPointLog):记录积分变动明细(类型、金额、前后余额、原因等)。
|
||||
- 系统配置(SystemConfig):键值型配置,用于存储默认头像等全局设置。
|
||||
- 服务层(UserService):封装注册、登录、信息更新、头像更新、密码修改/重置、邮箱变更等业务逻辑。
|
||||
- 仓储层(UserRepository):提供用户与登录/积分日志的数据库操作,包含事务性积分更新。
|
||||
- 认证工具(Password/JWT):密码加密与校验、JWT签发与校验。
|
||||
- 接口层(AuthHandler):接收HTTP请求,绑定参数,调用服务层并返回统一响应。
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
|
||||
|
||||
## 架构总览
|
||||
下图展示从HTTP请求到服务层再到仓储层的整体调用链路,以及关键对象之间的关系。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "AuthHandler"
|
||||
participant S as "UserService"
|
||||
participant P as "Password"
|
||||
participant J as "JWTService"
|
||||
participant R as "UserRepository"
|
||||
participant SCR as "SystemConfigRepository"
|
||||
C->>H : "POST /api/v1/auth/register"
|
||||
H->>H : "绑定RegisterRequest并校验"
|
||||
H->>S : "RegisterUser(jwtService, username, password, email, avatar)"
|
||||
S->>R : "FindUserByUsername(username)"
|
||||
S->>R : "FindUserByEmail(email)"
|
||||
S->>P : "HashPassword(password)"
|
||||
S->>SCR : "GetSystemConfigByKey('default_avatar')"
|
||||
S->>R : "CreateUser(user)"
|
||||
S->>J : "GenerateToken(user.id, user.username, user.role)"
|
||||
S-->>H : "返回user, token"
|
||||
H-->>C : "返回LoginResponse"
|
||||
C->>H : "POST /api/v1/auth/login"
|
||||
H->>S : "LoginUser(jwtService, usernameOrEmail, password, ip, ua)"
|
||||
S->>R : "按用户名或邮箱查找用户"
|
||||
S->>S : "检查用户状态"
|
||||
S->>P : "CheckPassword(user.password, password)"
|
||||
S->>R : "UpdateUserFields(last_login_at)"
|
||||
S->>J : "GenerateToken(...)"
|
||||
S-->>H : "返回user, token"
|
||||
H-->>C : "返回LoginResponse"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 用户服务(UserService)职责与方法
|
||||
- 注册(RegisterUser)
|
||||
- 校验用户名与邮箱唯一性
|
||||
- 密码加密
|
||||
- 头像选择:若传入自定义头像则使用,否则从系统配置读取默认头像
|
||||
- 初始化角色为“user”,状态为1(正常),积分初始为0
|
||||
- 创建用户并生成JWT
|
||||
- 登录(LoginUser)
|
||||
- 支持用户名或邮箱登录(通过是否包含“@”判断)
|
||||
- 校验用户状态(仅允许状态为1的用户登录)
|
||||
- 验证密码
|
||||
- 生成JWT并更新最后登录时间
|
||||
- 记录登录日志(成功/失败)
|
||||
- 查询与更新
|
||||
- 按ID获取用户
|
||||
- 更新用户信息(完整保存)
|
||||
- 更新头像(字段更新)
|
||||
- 修改密码(校验旧密码后加密新密码并更新)
|
||||
- 重置密码(通过邮箱查找用户并更新密码)
|
||||
- 更换邮箱(校验新邮箱唯一性后更新)
|
||||
|
||||
章节来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
|
||||
### 数据模型与默认值
|
||||
- 用户模型字段与默认值
|
||||
- 角色:默认“user”
|
||||
- 状态:默认1(正常)
|
||||
- 积分:默认0
|
||||
- 头像:默认空字符串
|
||||
- 时间戳:createdAt/updatedAt默认当前时间
|
||||
- 登录日志与积分日志
|
||||
- 登录日志包含IP、UA、登录方式、是否成功、失败原因
|
||||
- 积分日志包含变更类型、金额、前后余额、原因等
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
|
||||
### 头像逻辑(默认头像策略)
|
||||
- 优先使用用户提供的头像URL
|
||||
- 若未提供,则从系统配置中读取键为“default_avatar”的值作为默认头像
|
||||
- 若系统配置不存在,则返回提示信息字符串
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["开始"]) --> CheckProvided["是否提供了头像URL?"]
|
||||
CheckProvided --> |是| UseProvided["使用提供的头像URL"]
|
||||
CheckProvided --> |否| LoadConfig["从系统配置读取default_avatar"]
|
||||
LoadConfig --> Found{"配置是否存在?"}
|
||||
Found --> |是| UseDefault["使用系统配置中的默认头像URL"]
|
||||
Found --> |否| UseError["返回默认头像配置缺失提示"]
|
||||
UseProvided --> End(["结束"])
|
||||
UseDefault --> End
|
||||
UseError --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L228-L240)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L23)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
|
||||
章节来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L228-L240)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L23)
|
||||
- [internal/model/system_config.go](file://internal/model/system_config.go#L1-L42)
|
||||
|
||||
### 数据验证规则
|
||||
- 请求体绑定规则(由接口层负责)
|
||||
- 注册:用户名必填且长度在3-50之间,邮箱必填且符合邮箱格式,密码必填且长度6-128,验证码必填且长度6,头像可选且需为合法URL
|
||||
- 登录:用户名必填(支持用户名或邮箱),密码必填且长度6-128
|
||||
- 更新用户:头像可选且为合法URL,修改密码时旧密码与新密码长度要求
|
||||
- 服务层内部验证
|
||||
- 注册时对用户名、邮箱、密码进行基本非空与邮箱格式校验
|
||||
- 登录时对状态进行检查,密码进行校验
|
||||
- 更换邮箱时确保新邮箱未被他人使用
|
||||
|
||||
章节来源
|
||||
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go#L1-L200)
|
||||
|
||||
### 服务层与仓储层调用关系示例
|
||||
- 注册流程
|
||||
- Handler -> UserService.RegisterUser -> UserRepository.FindUserByUsername/ByEmail -> Password.HashPassword -> SystemConfigRepository.GetSystemConfigByKey -> UserRepository.CreateUser -> JWTService.GenerateToken
|
||||
- 登录流程
|
||||
- Handler -> UserService.LoginUser -> UserRepository.FindUserByUsername/ByEmail -> Password.CheckPassword -> UserRepository.UpdateUserFields -> JWTService.GenerateToken
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class AuthHandler {
|
||||
+Register()
|
||||
+Login()
|
||||
}
|
||||
class UserService {
|
||||
+RegisterUser(...)
|
||||
+LoginUser(...)
|
||||
+GetUserByID(...)
|
||||
+UpdateUserInfo(...)
|
||||
+UpdateUserAvatar(...)
|
||||
+ChangeUserPassword(...)
|
||||
+ResetUserPassword(...)
|
||||
+ChangeUserEmail(...)
|
||||
}
|
||||
class UserRepository {
|
||||
+CreateUser(...)
|
||||
+FindUserByID(...)
|
||||
+FindUserByUsername(...)
|
||||
+FindUserByEmail(...)
|
||||
+UpdateUser(...)
|
||||
+UpdateUserFields(...)
|
||||
+CreateLoginLog(...)
|
||||
+UpdateUserPoints(...)
|
||||
}
|
||||
class SystemConfigRepository {
|
||||
+GetSystemConfigByKey(...)
|
||||
}
|
||||
class Password {
|
||||
+HashPassword(...)
|
||||
+CheckPassword(...)
|
||||
}
|
||||
class JWTService {
|
||||
+GenerateToken(...)
|
||||
+ValidateToken(...)
|
||||
}
|
||||
AuthHandler --> UserService : "调用"
|
||||
UserService --> UserRepository : "CRUD/事务"
|
||||
UserService --> SystemConfigRepository : "读取默认头像"
|
||||
UserService --> Password : "密码处理"
|
||||
UserService --> JWTService : "签发Token"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
|
||||
## 依赖分析
|
||||
- 内部依赖
|
||||
- UserService 依赖 UserRepository、SystemConfigRepository、Password、JWTService、User模型、SystemConfig模型
|
||||
- AuthHandler 依赖 UserService、Types(请求/响应)、Logger、Redis(验证码)、Email(发送验证码)
|
||||
- 外部依赖
|
||||
- bcrypt 用于密码加密与校验
|
||||
- golang-jwt/jwt/v5 用于JWT签发与校验
|
||||
- GORM 用于数据库访问与事务
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
H["AuthHandler"] --> S["UserService"]
|
||||
S --> R["UserRepository"]
|
||||
S --> SCR["SystemConfigRepository"]
|
||||
S --> P["Password(bcrypt)"]
|
||||
S --> J["JWTService(gojwt)"]
|
||||
S --> M["User/SystemConfig Models"]
|
||||
R --> DB["GORM/DB"]
|
||||
SCR --> DB
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L1-L250)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [internal/repository/system_config_repository.go](file://internal/repository/system_config_repository.go#L1-L58)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
|
||||
章节来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/repository/user_repository.go](file://internal/repository/user_repository.go#L1-L137)
|
||||
- [pkg/auth/password.go](file://pkg/auth/password.go#L1-L21)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L1-L71)
|
||||
|
||||
## 性能考虑
|
||||
- 密码加密成本:bcrypt 默认成本较高,建议在高并发场景下关注注册/登录的延迟,必要时评估成本参数与异步化策略
|
||||
- 数据库索引:用户名与邮箱字段具备唯一索引,有助于快速去重;登录日志与积分日志按时间建立索引,有利于查询与统计
|
||||
- 事务一致性:积分变更采用事务,避免并发写入导致的余额不一致
|
||||
- 缓存策略:默认头像配置可考虑缓存以减少数据库查询次数
|
||||
|
||||
## 故障排查指南
|
||||
- 注册失败
|
||||
- 检查用户名/邮箱是否重复
|
||||
- 确认密码加密是否成功
|
||||
- 检查系统配置中是否存在“default_avatar”
|
||||
- 登录失败
|
||||
- 用户不存在或状态非1
|
||||
- 密码校验失败
|
||||
- 检查IP与User-Agent是否正确传入
|
||||
- 头像显示异常
|
||||
- 自定义头像URL是否有效
|
||||
- 系统默认头像配置是否正确
|
||||
- 集成测试参考
|
||||
- 单元测试覆盖了默认头像逻辑、邮箱检测、常量与验证逻辑,可据此定位问题
|
||||
|
||||
章节来源
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/service/user_service_test.go](file://internal/service/user_service_test.go#L1-L200)
|
||||
|
||||
## 结论
|
||||
UserService 以清晰的职责划分实现了用户生命周期管理:从注册、登录到信息维护与安全控制。通过明确的默认值与验证规则,结合仓储层的事务与模型层的结构化设计,系统在保证安全性的同时具备良好的可扩展性。对于扩展需求,可在服务层增加新的默认值或属性映射,并在仓储层补充相应的查询/更新逻辑。
|
||||
|
||||
## 附录
|
||||
|
||||
### 扩展点指导
|
||||
- 修改默认角色
|
||||
- 在注册流程中调整角色初始化值
|
||||
- 若需动态分配角色,可引入系统配置或权限策略模块
|
||||
- 添加新的用户属性
|
||||
- 在 User 模型中新增字段并设置默认值
|
||||
- 在注册流程中初始化该字段
|
||||
- 在接口层的请求/响应结构中同步新增字段
|
||||
- 默认积分初始化
|
||||
- 当前初始积分为0,可通过系统配置读取或硬编码调整
|
||||
- 注册奖励积分可作为后续增强点(服务层已有TODO标记)
|
||||
|
||||
章节来源
|
||||
- [internal/model/user.go](file://internal/model/user.go#L1-L71)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [internal/types/common.go](file://internal/types/common.go#L1-L215)
|
||||
- [internal/service/common.go](file://internal/service/common.go#L1-L14)
|
||||
@@ -1,310 +0,0 @@
|
||||
# JWT认证
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [manager.go](file://pkg/auth/manager.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [JWT服务结构设计](#jwt服务结构设计)
|
||||
3. [令牌生成与验证机制](#令牌生成与验证机制)
|
||||
4. [认证中间件实现](#认证中间件实现)
|
||||
5. [JWT工作流程](#jwt工作流程)
|
||||
6. [密钥管理与安全策略](#密钥管理与安全策略)
|
||||
7. [单例模式应用](#单例模式应用)
|
||||
8. [最佳实践](#最佳实践)
|
||||
|
||||
## 简介
|
||||
CarrotSkin项目采用基于JWT(JSON Web Token)的用户认证机制,为系统提供安全、无状态的用户身份验证。该机制通过在客户端和服务器之间传递加密的令牌来验证用户身份,避免了传统会话存储的服务器状态管理开销。本文档详细阐述了JWT服务的实现原理、核心组件设计以及在Gin框架中的集成方式,为开发者提供全面的技术参考。
|
||||
|
||||
## JWT服务结构设计
|
||||
|
||||
JWT服务的核心是`JWTService`结构体,它封装了令牌生成和验证所需的所有配置和方法。该服务通过`Claims`结构体定义了令牌中包含的用户声明信息。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class JWTService {
|
||||
-secretKey string
|
||||
-expireHours int
|
||||
+GenerateToken(userID int64, username string, role string) (string, error)
|
||||
+ValidateToken(tokenString string) (*Claims, error)
|
||||
}
|
||||
class Claims {
|
||||
+UserID int64
|
||||
+Username string
|
||||
+Role string
|
||||
+RegisteredClaims jwt.RegisteredClaims
|
||||
}
|
||||
JWTService --> Claims : "包含"
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L11-L14)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L25-L30)
|
||||
|
||||
**Section sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L11-L71)
|
||||
|
||||
## 令牌生成与验证机制
|
||||
|
||||
### 令牌生成(GenerateToken)
|
||||
`GenerateToken`方法负责创建JWT令牌,它接收用户ID、用户名和角色作为参数,构建包含这些信息的声明(Claims),并使用预设的密钥进行签名。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始生成令牌]) --> CreateClaims["创建声明对象<br/>包含user_id、username、role"]
|
||||
CreateClaims --> SetExpiry["设置过期时间<br/>(当前时间 + expireHours)"]
|
||||
SetExpiry --> SetIssued["设置签发时间<br/>(当前时间)"]
|
||||
SetIssued --> SetNotBefore["设置生效时间<br/>(当前时间)"]
|
||||
SetNotBefore --> SetIssuer["设置发行者<br/>(carrotskin)"]
|
||||
SetIssuer --> CreateToken["创建JWT令牌<br/>使用HS256算法"]
|
||||
CreateToken --> SignToken["使用secretKey签名"]
|
||||
SignToken --> ReturnToken["返回签名后的令牌字符串"]
|
||||
ReturnToken --> End([结束])
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L33-L53)
|
||||
|
||||
### 令牌验证(ValidateToken)
|
||||
`ValidateToken`方法负责验证接收到的JWT令牌的有效性,包括签名验证、过期检查和声明解析。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始验证令牌]) --> ParseToken["解析令牌字符串"]
|
||||
ParseToken --> CheckSignature["验证签名<br/>使用secretKey"]
|
||||
CheckSignature --> IsValid{"签名有效?"}
|
||||
IsValid --> |否| ReturnError["返回错误<br/>无效的token"]
|
||||
IsValid --> |是| CheckClaims["检查声明对象"]
|
||||
CheckClaims --> IsClaimsValid{"声明类型正确<br/>且令牌有效?"}
|
||||
IsClaimsValid --> |否| ReturnError
|
||||
IsClaimsValid --> |是| ReturnClaims["返回解析出的Claims对象"]
|
||||
ReturnClaims --> End([结束])
|
||||
ReturnError --> End
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L56-L70)
|
||||
|
||||
**Section sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L32-L70)
|
||||
|
||||
## 认证中间件实现
|
||||
|
||||
CarrotSkin项目提供了两种认证中间件:强制认证中间件(AuthMiddleware)和可选认证中间件(OptionalAuthMiddleware),它们在Gin框架中拦截HTTP请求并处理Authorization头。
|
||||
|
||||
### 强制认证中间件(AuthMiddleware)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Middleware as "AuthMiddleware"
|
||||
participant JWTService as "JWTService"
|
||||
Client->>Middleware : 发送请求<br/>Authorization : Bearer token
|
||||
Middleware->>Middleware : 获取Authorization头
|
||||
alt 头部为空
|
||||
Middleware-->>Client : 返回401<br/>缺少Authorization头
|
||||
return
|
||||
end
|
||||
Middleware->>Middleware : 解析Bearer格式
|
||||
alt 格式无效
|
||||
Middleware-->>Client : 返回401<br/>无效的Authorization头格式
|
||||
return
|
||||
end
|
||||
Middleware->>JWTService : 调用ValidateToken(token)
|
||||
alt 验证失败
|
||||
JWTService-->>Middleware : 返回错误
|
||||
Middleware-->>Client : 返回401<br/>无效的token
|
||||
return
|
||||
end
|
||||
JWTService-->>Middleware : 返回Claims对象
|
||||
Middleware->>Middleware : 将用户信息存入上下文<br/>(user_id, username, role)
|
||||
Middleware->>Client : 继续处理请求
|
||||
```
|
||||
|
||||
### 可选认证中间件(OptionalAuthMiddleware)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Middleware as "OptionalAuthMiddleware"
|
||||
participant JWTService as "JWTService"
|
||||
Client->>Middleware : 发送请求<br/>Authorization : Bearer token
|
||||
Middleware->>Middleware : 获取Authorization头
|
||||
alt 头部存在
|
||||
Middleware->>Middleware : 解析Bearer格式
|
||||
alt 格式正确
|
||||
Middleware->>JWTService : 调用ValidateToken(token)
|
||||
alt 验证成功
|
||||
JWTService-->>Middleware : 返回Claims对象
|
||||
Middleware->>Middleware : 将用户信息存入上下文
|
||||
end
|
||||
end
|
||||
end
|
||||
Middleware->>Client : 继续处理请求<br/>(无论认证成功与否)
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [auth.go](file://internal/middleware/auth.go#L13-L55)
|
||||
- [auth.go](file://internal/middleware/auth.go#L58-L78)
|
||||
|
||||
**Section sources**
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
|
||||
## JWT工作流程
|
||||
|
||||
以下是用户从登录到访问受保护资源的完整JWT工作流程:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as "用户"
|
||||
participant Frontend as "前端"
|
||||
participant Backend as "后端"
|
||||
User->>Frontend : 输入用户名/邮箱和密码
|
||||
Frontend->>Backend : POST /api/v1/auth/login
|
||||
Backend->>Backend : 调用LoginUser服务
|
||||
Backend->>Backend : 验证用户凭证
|
||||
alt 验证成功
|
||||
Backend->>Backend : 调用JWTService.GenerateToken
|
||||
Backend->>Backend : 生成JWT令牌
|
||||
Backend-->>Frontend : 返回200<br/>{token, userInfo}
|
||||
Frontend->>Frontend : 存储令牌
|
||||
User->>Frontend : 访问受保护页面
|
||||
Frontend->>Backend : GET /api/v1/protected<br/>Authorization : Bearer token
|
||||
Backend->>Backend : AuthMiddleware拦截请求
|
||||
Backend->>Backend : 解析Authorization头
|
||||
Backend->>Backend : 调用JWTService.ValidateToken
|
||||
Backend->>Backend : 验证令牌有效性
|
||||
Backend->>Backend : 提取用户信息存入上下文
|
||||
Backend->>Backend : 继续处理业务逻辑
|
||||
Backend-->>Frontend : 返回请求数据
|
||||
else 验证失败
|
||||
Backend-->>Frontend : 返回401<br/>登录失败
|
||||
end
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [user_service.go](file://internal/service/user_service.go#L71-L122)
|
||||
- [auth.go](file://internal/middleware/auth.go#L13-L55)
|
||||
|
||||
**Section sources**
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [user_service.go](file://internal/service/user_service.go#L71-L122)
|
||||
|
||||
## 密钥管理与安全策略
|
||||
|
||||
### 配置管理
|
||||
JWT服务的配置通过`config.go`文件中的`JWTConfig`结构体进行管理,支持环境变量注入,确保密钥的安全性。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class JWTConfig {
|
||||
+Secret string
|
||||
+ExpireHours int
|
||||
}
|
||||
class Config {
|
||||
+Server ServerConfig
|
||||
+Database DatabaseConfig
|
||||
+Redis RedisConfig
|
||||
+JWT JWTConfig
|
||||
+Casbin CasbinConfig
|
||||
+Log LogConfig
|
||||
+Upload UploadConfig
|
||||
+Email EmailConfig
|
||||
}
|
||||
Config --> JWTConfig : "包含"
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L68-L71)
|
||||
- [config.go](file://pkg/config/config.go#L14-L24)
|
||||
|
||||
### 安全声明
|
||||
JWT令牌中包含的声明(Claims)具有重要的安全意义:
|
||||
|
||||
- **user_id**: 用户唯一标识,用于在系统中识别用户身份
|
||||
- **username**: 用户名,用于显示和审计
|
||||
- **role**: 用户角色,用于基于角色的访问控制(RBAC)
|
||||
- **ExpiresAt**: 过期时间,防止令牌长期有效
|
||||
- **IssuedAt**: 签发时间,用于审计和令牌生命周期管理
|
||||
- **NotBefore**: 生效时间,可用于延迟生效的令牌
|
||||
- **Issuer**: 发行者,确保令牌来自可信源
|
||||
|
||||
**Section sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L25-L30)
|
||||
- [config.go](file://pkg/config/config.go#L68-L71)
|
||||
|
||||
## 单例模式应用
|
||||
|
||||
`MustGetJWTService`函数实现了线程安全的单例模式,确保在整个应用程序生命周期中只存在一个JWT服务实例。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Init as "初始化"
|
||||
participant Get as "GetJWTService"
|
||||
participant MustGet as "MustGetJWTService"
|
||||
participant Once as "sync.Once"
|
||||
Init->>Once : 调用Init(cfg)
|
||||
Once->>Once : 检查是否已执行
|
||||
alt 首次调用
|
||||
Once->>Init : 执行初始化
|
||||
Init->>jwtServiceInstance : 创建JWTService实例
|
||||
Init-->>Once : 标记已执行
|
||||
else 非首次调用
|
||||
Once-->>Init : 跳过执行
|
||||
end
|
||||
Get->>Get : 检查jwtServiceInstance
|
||||
alt 实例存在
|
||||
Get-->>Get : 返回实例
|
||||
else 实例不存在
|
||||
Get-->>Get : 返回错误
|
||||
end
|
||||
MustGet->>Get : 调用GetJWTService
|
||||
alt 获取成功
|
||||
Get-->>MustGet : 返回实例
|
||||
MustGet-->>MustGet : 返回实例
|
||||
else 获取失败
|
||||
Get-->>MustGet : 返回错误
|
||||
MustGet->>MustGet : panic(错误)
|
||||
end
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [manager.go](file://pkg/auth/manager.go#L9-L45)
|
||||
|
||||
**Section sources**
|
||||
- [manager.go](file://pkg/auth/manager.go#L9-L45)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 密钥管理
|
||||
- JWT密钥(`secretKey`)应通过环境变量配置,避免硬编码在代码中
|
||||
- 使用足够长度的随机字符串作为密钥,提高安全性
|
||||
- 定期轮换密钥(虽然当前实现不支持,但架构上可以扩展)
|
||||
|
||||
### 过期策略
|
||||
- 当前默认过期时间为168小时(7天),可通过`JWT_EXPIRE_HOURS`环境变量调整
|
||||
- 对于敏感操作,建议使用短期令牌或结合刷新令牌机制
|
||||
- 过期时间不宜过长,以降低令牌泄露后的风险
|
||||
|
||||
### 防止重放攻击
|
||||
- 虽然JWT本身是无状态的,但可以通过以下方式增强安全性:
|
||||
- 使用短期令牌
|
||||
- 在Redis中维护已注销令牌的黑名单
|
||||
- 结合客户端指纹(如IP地址、User-Agent)进行额外验证
|
||||
- 当前实现中,`OptionalAuthMiddleware`提供了基础的灵活性,允许部分接口在认证失败时仍能访问
|
||||
|
||||
### 性能考虑
|
||||
- JWT验证是计算密集型操作,但在现代硬件上性能良好
|
||||
- 使用HS256算法在安全性和性能之间取得了良好平衡
|
||||
- 避免在令牌中存储过多信息,以减小令牌大小和传输开销
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L164)
|
||||
- [manager.go](file://pkg/auth/manager.go#L18-L23)
|
||||
- [jwt.go](file://pkg/auth/jwt.go#L39-L43)
|
||||
@@ -1,211 +0,0 @@
|
||||
# RBAC权限控制
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [audit_log.go](file://internal/model/audit_log.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [项目结构](#项目结构)
|
||||
2. [核心组件](#核心组件)
|
||||
3. [架构概述](#架构概述)
|
||||
4. [详细组件分析](#详细组件分析)
|
||||
5. [依赖分析](#依赖分析)
|
||||
6. [性能考虑](#性能考虑)
|
||||
7. [故障排除指南](#故障排除指南)
|
||||
8. [结论](#结论)
|
||||
|
||||
## 项目结构
|
||||
|
||||
项目采用分层架构设计,权限控制相关文件主要分布在`configs/casbin/`和`internal/middleware/`目录下。`rbac_model.conf`文件定义了基于Casbin的RBAC权限模型,而`auth.go`文件实现了JWT认证中间件,为权限系统提供用户身份验证。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置"
|
||||
rbac_model_conf[rbac_model.conf]
|
||||
end
|
||||
subgraph "中间件"
|
||||
auth_go[auth.go]
|
||||
end
|
||||
subgraph "服务层"
|
||||
user_service_go[user_service.go]
|
||||
end
|
||||
subgraph "数据模型"
|
||||
audit_log_go[audit_log.go]
|
||||
end
|
||||
rbac_model_conf --> auth_go
|
||||
auth_go --> user_service_go
|
||||
user_service_go --> audit_log_go
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [audit_log.go](file://internal/model/audit_log.go)
|
||||
|
||||
**本节来源**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
|
||||
## 核心组件
|
||||
|
||||
系统的核心权限控制组件包括Casbin权限模型配置、JWT认证中间件和用户服务。`rbac_model.conf`文件定义了请求定义、策略定义、角色定义、匹配器和策略效果等关键部分。`auth.go`文件中的`AuthMiddleware`函数实现了JWT认证逻辑,确保只有经过身份验证的用户才能访问受保护的资源。`user_service.go`文件中的用户服务函数处理用户注册、登录等操作,并在注册时为新用户分配默认角色。
|
||||
|
||||
**本节来源**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
|
||||
## 架构概述
|
||||
|
||||
系统采用基于Casbin的RBAC(基于角色的访问控制)权限模型。该模型通过`rbac_model.conf`文件定义,包含请求定义、策略定义、角色定义、匹配器和策略效果五个部分。JWT认证中间件负责用户身份验证,将用户信息存储在请求上下文中。用户服务处理用户相关的业务逻辑,并在注册时为用户分配角色。权限检查在中间件或服务层进行,通过将用户角色(role)与访问资源(obj)和操作(act)进行匹配来决定是否允许访问。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[客户端] --> B[JWT认证中间件]
|
||||
B --> C{身份验证}
|
||||
C --> |成功| D[权限检查]
|
||||
C --> |失败| E[返回401]
|
||||
D --> F{权限允许?}
|
||||
F --> |是| G[执行操作]
|
||||
F --> |否| H[返回403]
|
||||
G --> I[返回结果]
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### RBAC模型配置分析
|
||||
|
||||
`rbac_model.conf`文件定义了系统的权限模型。请求定义`r = sub, obj, act`表示权限检查请求包含主体(用户或角色)、对象(资源)和动作(操作)三个部分。策略定义`p = sub, obj, act`定义了权限策略的结构。角色定义`g = _, _`用于建立角色继承关系。匹配器`m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act`是权限检查的核心逻辑,它首先通过`g(r.sub, p.sub)`检查请求主体是否具有策略主体的角色,然后检查请求的对象和动作是否与策略匹配。策略效果`e = some(where (p.eft == allow))`表示只要存在一条允许的策略,就允许访问。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class RBACModel {
|
||||
+request_definition : r = sub, obj, act
|
||||
+policy_definition : p = sub, obj, act
|
||||
+role_definition : g = _, _
|
||||
+matchers : m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
|
||||
+policy_effect : e = some(where (p.eft == allow))
|
||||
}
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
|
||||
**本节来源**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
|
||||
### JWT认证中间件分析
|
||||
|
||||
`auth.go`文件中的`AuthMiddleware`函数实现了JWT认证中间件。该中间件从请求头中提取JWT令牌,验证其有效性,并将用户信息存储在请求上下文中。`OptionalAuthMiddleware`函数提供了可选的JWT认证,允许未认证用户访问某些资源。这两个中间件为系统的权限控制提供了基础的身份验证功能。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 客户端
|
||||
participant Middleware as 认证中间件
|
||||
participant JWTService as JWT服务
|
||||
Client->>Middleware : 发送请求(Authorization头)
|
||||
Middleware->>Middleware : 检查Authorization头
|
||||
alt 头部不存在
|
||||
Middleware-->>Client : 返回401(缺少Authorization头)
|
||||
else 有效格式
|
||||
Middleware->>JWTService : 验证令牌
|
||||
alt 令牌有效
|
||||
JWTService-->>Middleware : 返回用户声明
|
||||
Middleware->>Middleware : 将用户信息存入上下文
|
||||
Middleware->>Client : 继续处理请求
|
||||
else 令牌无效
|
||||
Middleware-->>Client : 返回401(无效的token)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
|
||||
**本节来源**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
|
||||
### 用户服务分析
|
||||
|
||||
`user_service.go`文件中的`RegisterUser`函数处理用户注册逻辑。在创建用户时,该函数将用户的`Role`字段设置为"user",为新用户分配默认角色。`LoginUser`函数处理用户登录,验证用户名/邮箱和密码的正确性,并在成功登录后生成JWT令牌。这些服务函数与权限系统紧密集成,确保用户在注册和登录时正确地获得和验证其角色。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([注册用户]) --> CheckUsername["检查用户名是否已存在"]
|
||||
CheckUsername --> UsernameExists{"用户名已存在?"}
|
||||
UsernameExists --> |是| ReturnError1["返回错误: 用户名已存在"]
|
||||
UsernameExists --> |否| CheckEmail["检查邮箱是否已存在"]
|
||||
CheckEmail --> EmailExists{"邮箱已存在?"}
|
||||
EmailExists --> |是| ReturnError2["返回错误: 邮箱已被注册"]
|
||||
EmailExists --> |否| HashPassword["加密密码"]
|
||||
HashPassword --> CreateAvatar["确定头像URL"]
|
||||
CreateAvatar --> CreateUser["创建用户(角色=user)"]
|
||||
CreateUser --> GenerateToken["生成JWT Token"]
|
||||
GenerateToken --> ReturnSuccess["返回用户信息和Token"]
|
||||
ReturnError1 --> End([结束])
|
||||
ReturnError2 --> End
|
||||
ReturnSuccess --> End
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
|
||||
**本节来源**
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
|
||||
## 依赖分析
|
||||
|
||||
系统各组件之间存在明确的依赖关系。`auth.go`依赖于`pkg/auth/jwt.go`提供的JWT服务进行令牌验证。`user_service.go`依赖于`repository/user_repository.go`进行数据库操作,并依赖于`pkg/auth/password.go`进行密码加密。`rbac_model.conf`被Casbin引擎读取,用于定义权限模型。这些依赖关系确保了系统的模块化和可维护性。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[auth.go] --> B[jwt.go]
|
||||
C[user_service.go] --> D[user_repository.go]
|
||||
C --> E[password.go]
|
||||
F[rbac_model.conf] --> G[Casbin引擎]
|
||||
```
|
||||
|
||||
**图示来源**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
|
||||
**本节来源**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [user_repository.go](file://internal/repository/user_repository.go)
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
|
||||
## 性能考虑
|
||||
|
||||
系统的权限控制设计考虑了性能因素。JWT令牌在客户端存储,减少了服务器的认证开销。Casbin的权限检查算法经过优化,能够快速匹配权限策略。数据库查询使用了适当的索引,如`casbin_rule`表上的`ptype`、`v0`、`v1`索引,提高了权限检查的效率。对于高并发场景,可以考虑将权限策略缓存到Redis中,进一步提高性能。
|
||||
|
||||
## 故障排除指南
|
||||
|
||||
当遇到权限相关问题时,可以按照以下步骤进行排查:
|
||||
1. 检查JWT令牌是否正确生成和传递。
|
||||
2. 验证`rbac_model.conf`文件的语法是否正确。
|
||||
3. 检查数据库中的`casbin_rule`表是否包含正确的权限策略。
|
||||
4. 确认用户的角色是否正确分配。
|
||||
5. 查看系统日志,寻找权限拒绝的记录。
|
||||
|
||||
**本节来源**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
- [audit_log.go](file://internal/model/audit_log.go)
|
||||
|
||||
## 结论
|
||||
|
||||
本系统采用基于Casbin的RBAC权限模型,通过`rbac_model.conf`文件定义权限策略,使用JWT认证中间件进行身份验证。该设计提供了灵活、可扩展的权限控制机制,能够满足不同场景下的权限管理需求。通过合理配置权限策略,可以实现细粒度的访问控制,确保系统的安全性。
|
||||
@@ -1,326 +0,0 @@
|
||||
# 认证与授权
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [manager.go](file://pkg/auth/manager.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [token.go](file://internal/model/token.go)
|
||||
- [token_service.go](file://internal/service/token_service.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [user.go](file://internal/model/user.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [audit_log.go](file://internal/model/audit_log.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构概述](#架构概述)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排除指南](#故障排除指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
CarrotSkin项目实现了一套完整的认证与授权安全机制,采用JWT(JSON Web Token)进行用户认证,并结合Casbin实现基于角色的访问控制(RBAC)。该系统为用户提供安全的注册、登录、密码重置功能,同时通过中间件对API请求进行权限验证。本文档详细说明了系统的安全架构、JWT认证流程、Casbin权限模型配置以及认证中间件的实现细节。
|
||||
|
||||
## 项目结构
|
||||
CarrotSkin项目的认证与授权相关代码分布在多个目录中,形成了清晰的分层架构。核心安全功能主要集中在`pkg/auth`包中,而具体的业务逻辑则分布在`internal`目录下的各个模块。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置"
|
||||
config[configs/casbin/rbac_model.conf]
|
||||
end
|
||||
subgraph "内部处理"
|
||||
handler[internal/handler]
|
||||
middleware[internal/middleware]
|
||||
model[internal/model]
|
||||
service[internal/service]
|
||||
repository[internal/repository]
|
||||
end
|
||||
subgraph "公共包"
|
||||
auth[pkg/auth]
|
||||
config_pkg[pkg/config]
|
||||
database[pkg/database]
|
||||
redis[pkg/redis]
|
||||
end
|
||||
config --> auth
|
||||
auth --> middleware
|
||||
middleware --> handler
|
||||
handler --> service
|
||||
service --> repository
|
||||
repository --> database
|
||||
service --> redis
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
|
||||
**Section sources**
|
||||
- [project_structure](file://project_structure)
|
||||
|
||||
## 核心组件
|
||||
CarrotSkin项目的认证与授权系统由多个核心组件构成,包括JWT服务、认证中间件、Casbin权限管理器、用户服务和令牌服务。这些组件协同工作,确保系统的安全性。
|
||||
|
||||
**Section sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [token_service.go](file://internal/service/token_service.go)
|
||||
|
||||
## 架构概述
|
||||
CarrotSkin的认证与授权架构采用分层设计,从下到上的层次包括:配置层、认证服务层、中间件层、处理器层和路由层。这种设计实现了关注点分离,提高了代码的可维护性和可测试性。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[客户端] --> B[HTTP请求]
|
||||
B --> C{路由}
|
||||
C --> D[/认证路由\n无需JWT/]
|
||||
C --> E[/受保护路由\n需要JWT/]
|
||||
D --> F[auth_handler]
|
||||
E --> G[AuthMiddleware]
|
||||
G --> H{验证JWT}
|
||||
H --> |有效| I[业务处理器]
|
||||
H --> |无效| J[返回401]
|
||||
I --> K[service]
|
||||
K --> L[repository]
|
||||
L --> M[(数据库)]
|
||||
G --> N[Casbin权限检查]
|
||||
N --> |允许| I
|
||||
N --> |拒绝| O[返回403]
|
||||
P[JWT配置] --> Q[jwt.go]
|
||||
Q --> R[AuthMiddleware]
|
||||
S[Casbin配置] --> T[CasbinRule]
|
||||
T --> N
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [routes.go](file://internal/handler/routes.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [audit_log.go](file://internal/model/audit_log.go)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### JWT认证服务分析
|
||||
JWT认证服务是CarrotSkin安全机制的核心,负责生成和验证JWT令牌。该服务实现了标准的JWT功能,包括令牌的签发、验证和声明管理。
|
||||
|
||||
#### JWT服务类图
|
||||
```mermaid
|
||||
classDiagram
|
||||
class JWTService {
|
||||
+string secretKey
|
||||
+int expireHours
|
||||
+GenerateToken(userID int64, username string, role string) (string, error)
|
||||
+ValidateToken(tokenString string) (*Claims, error)
|
||||
}
|
||||
class Claims {
|
||||
+int64 UserID
|
||||
+string Username
|
||||
+string Role
|
||||
+jwt.RegisteredClaims
|
||||
}
|
||||
class JWTManager {
|
||||
-static *JWTService jwtServiceInstance
|
||||
-static sync.Once once
|
||||
+Init(cfg JWTConfig) error
|
||||
+GetJWTService() (*JWTService, error)
|
||||
+MustGetJWTService() *JWTService
|
||||
}
|
||||
JWTManager --> JWTService : "创建"
|
||||
JWTService --> Claims : "包含"
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [manager.go](file://pkg/auth/manager.go)
|
||||
|
||||
**Section sources**
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [manager.go](file://pkg/auth/manager.go)
|
||||
|
||||
### 认证中间件分析
|
||||
认证中间件负责在请求处理流程中验证JWT令牌的有效性。它拦截所有受保护的API请求,确保只有携带有效令牌的请求才能访问受保护的资源。
|
||||
|
||||
#### 认证中间件流程图
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> GetHeader["获取Authorization头"]
|
||||
GetHeader --> HeaderEmpty{"头为空?"}
|
||||
HeaderEmpty --> |是| Return401Unauthorized["返回401未授权"]
|
||||
HeaderEmpty --> |否| ParseHeader["解析Bearer格式"]
|
||||
ParseHeader --> FormatValid{"格式有效?"}
|
||||
FormatValid --> |否| Return401InvalidFormat["返回401无效格式"]
|
||||
FormatValid --> |是| ExtractToken["提取Token"]
|
||||
ExtractToken --> ValidateToken["验证Token"]
|
||||
ValidateToken --> TokenValid{"Token有效?"}
|
||||
TokenValid --> |否| Return401InvalidToken["返回401无效Token"]
|
||||
TokenValid --> |是| StoreUserInfo["将用户信息存入上下文"]
|
||||
StoreUserInfo --> Next["调用下一个处理器"]
|
||||
Next --> End([结束])
|
||||
Return401Unauthorized --> End
|
||||
Return401InvalidFormat --> End
|
||||
Return401InvalidToken --> End
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
|
||||
**Section sources**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
|
||||
### Casbin RBAC权限模型分析
|
||||
CarrotSkin使用Casbin实现基于角色的访问控制(RBAC)模型,通过配置文件定义权限策略,实现灵活的权限管理。
|
||||
|
||||
#### Casbin RBAC模型配置
|
||||
```conf
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
|
||||
**Section sources**
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
|
||||
### 用户认证流程分析
|
||||
用户认证流程涵盖了从注册、登录到密码重置的完整生命周期,确保用户身份的安全管理。
|
||||
|
||||
#### 用户认证流程序列图
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Handler as "AuthHandler"
|
||||
participant Service as "UserService"
|
||||
participant JWT as "JWTService"
|
||||
participant DB as "数据库"
|
||||
Client->>Handler : POST /api/v1/auth/register
|
||||
Handler->>Service : RegisterUser()
|
||||
Service->>DB : 检查用户名/邮箱是否存在
|
||||
DB-->>Service : 返回检查结果
|
||||
Service->>JWT : HashPassword()
|
||||
Service->>DB : 创建用户记录
|
||||
DB-->>Service : 用户对象
|
||||
Service->>JWT : GenerateToken()
|
||||
JWT-->>Service : JWT Token
|
||||
Service-->>Handler : 用户和Token
|
||||
Handler-->>Client : 200 OK {token, userInfo}
|
||||
Client->>Handler : POST /api/v1/auth/login
|
||||
Handler->>Service : LoginUser()
|
||||
Service->>DB : 查找用户(用户名/邮箱)
|
||||
DB-->>Service : 用户对象
|
||||
Service->>JWT : CheckPassword()
|
||||
Service->>JWT : GenerateToken()
|
||||
JWT-->>Service : JWT Token
|
||||
Service-->>Handler : 用户和Token
|
||||
Handler-->>Client : 200 OK {token, userInfo}
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
|
||||
**Section sources**
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [user_service.go](file://internal/service/user_service.go)
|
||||
|
||||
## 依赖分析
|
||||
CarrotSkin的认证与授权系统依赖于多个外部包和内部组件,形成了复杂的依赖关系网络。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[pkg/auth] --> B[github.com/golang-jwt/jwt/v5]
|
||||
A --> C[pkg/config]
|
||||
D[internal/middleware] --> A
|
||||
D --> E[github.com/gin-gonic/gin]
|
||||
F[internal/handler] --> D
|
||||
F --> G[internal/service]
|
||||
G --> A
|
||||
G --> H[pkg/database]
|
||||
G --> I[pkg/redis]
|
||||
J[pkg/config] --> K[github.com/spf13/viper]
|
||||
J --> L[github.com/joho/godotenv]
|
||||
H --> M[gorm.io/gorm]
|
||||
I --> N[github.com/redis/go-redis]
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [go.mod](file://go.mod)
|
||||
- [jwt.go](file://pkg/auth/jwt.go)
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
|
||||
**Section sources**
|
||||
- [go.mod](file://go.mod)
|
||||
- [go.sum](file://go.sum)
|
||||
|
||||
## 性能考虑
|
||||
CarrotSkin的认证与授权系统在设计时考虑了性能因素,通过多种机制确保系统的高效运行。
|
||||
|
||||
1. **JWT令牌验证**:JWT令牌的验证是无状态的,不需要查询数据库,大大提高了验证速度。
|
||||
2. **Redis缓存**:系统使用Redis存储验证码等临时数据,减少了数据库的压力。
|
||||
3. **连接池**:数据库和Redis都使用了连接池,避免了频繁创建和销毁连接的开销。
|
||||
4. **异步操作**:某些非关键操作(如清理多余令牌)使用goroutine异步执行,不影响主流程性能。
|
||||
5. **批量操作**:在清理多余令牌时,使用批量删除操作,减少了数据库交互次数。
|
||||
|
||||
**Section sources**
|
||||
- [token_service.go](file://internal/service/token_service.go)
|
||||
- [repository/token_repository.go](file://internal/repository/token_repository.go)
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
|
||||
## 故障排除指南
|
||||
当遇到认证与授权相关的问题时,可以参考以下常见问题的解决方案:
|
||||
|
||||
1. **401 Unauthorized错误**:
|
||||
- 检查请求头中是否包含`Authorization`头
|
||||
- 确认`Authorization`头的格式是否为`Bearer <token>`
|
||||
- 验证JWT令牌是否已过期
|
||||
- 检查JWT密钥配置是否正确
|
||||
|
||||
2. **403 Forbidden错误**:
|
||||
- 检查用户角色是否有权限访问该资源
|
||||
- 验证Casbin策略配置是否正确
|
||||
- 确认请求的资源和操作是否匹配策略
|
||||
|
||||
3. **登录失败**:
|
||||
- 检查用户名/邮箱和密码是否正确
|
||||
- 确认用户账户状态是否正常(未被禁用)
|
||||
- 检查数据库连接是否正常
|
||||
|
||||
4. **令牌刷新失败**:
|
||||
- 确认旧令牌是否有效
|
||||
- 检查客户端令牌是否匹配
|
||||
- 验证用户是否有权限选择指定的角色
|
||||
|
||||
**Section sources**
|
||||
- [auth.go](file://internal/middleware/auth.go)
|
||||
- [auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [token_service.go](file://internal/service/token_service.go)
|
||||
|
||||
## 结论
|
||||
CarrotSkin项目实现了一套完整且安全的认证与授权机制,通过JWT和Casbin的结合,提供了灵活的用户身份验证和细粒度的权限控制。系统的分层架构使得各个组件职责清晰,易于维护和扩展。对于初学者,系统提供了清晰的API文档和错误处理机制;对于经验丰富的开发者,系统提供了扩展权限模型和集成其他认证方式的可能性。整体而言,CarrotSkin的安全机制设计合理,能够有效保护系统资源,为用户提供安全可靠的服务。
|
||||
@@ -1,276 +0,0 @@
|
||||
# 部署
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [start.sh](file://start.sh)
|
||||
- [run.sh](file://run.sh)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
- [scripts/dev.sh](file://scripts/dev.sh)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [环境准备](#环境准备)
|
||||
4. [部署流程](#部署流程)
|
||||
5. [启动脚本详解](#启动脚本详解)
|
||||
6. [生产环境配置](#生产环境配置)
|
||||
7. [健康检查与监控](#健康检查与监控)
|
||||
8. [日志管理](#日志管理)
|
||||
9. [高级部署策略](#高级部署策略)
|
||||
10. [故障排除](#故障排除)
|
||||
|
||||
## 简介
|
||||
CarrotSkin 是一个为 Minecraft 皮肤站设计的后端服务,提供用户管理、材质上传下载、档案管理等功能。本部署文档详细介绍了如何在生产环境中部署和运行 CarrotSkin 服务,包括使用 `start.sh` 和 `run.sh` 脚本的完整流程、环境变量配置、依赖项管理以及运维监控等方面的内容。文档既为初学者提供了简单的部署步骤,也为经验丰富的开发者提供了高可用性和可扩展性部署的高级策略。
|
||||
|
||||
## 项目结构
|
||||
CarrotSkin 项目采用标准的 Go 项目结构,主要包含以下目录:
|
||||
|
||||
- `configs/`:配置文件目录,包含 Casbin 权限模型等配置
|
||||
- `internal/`:核心业务逻辑,包括处理器、中间件、模型、仓库和服务
|
||||
- `pkg/`:可重用的包,如数据库、Redis、日志、配置管理等
|
||||
- `scripts/`:脚本目录,包含环境检查和开发环境启动脚本
|
||||
- 根目录:包含主启动脚本 `start.sh`、`run.sh` 和 Go 模块文件
|
||||
|
||||
项目通过环境变量进行配置,不依赖传统的 YAML 配置文件,这使得部署更加灵活,特别适合容器化环境。
|
||||
|
||||
**Section sources**
|
||||
- [start.sh](file://start.sh)
|
||||
- [run.sh](file://run.sh)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
- [scripts/dev.sh](file://scripts/dev.sh)
|
||||
|
||||
## 环境准备
|
||||
在部署 CarrotSkin 服务之前,需要确保以下依赖项已正确安装和配置:
|
||||
|
||||
1. **Go 语言环境**:项目需要 Go 1.23.0 或更高版本。可以通过 `go version` 命令检查当前 Go 版本。
|
||||
2. **PostgreSQL 数据库**:服务依赖 PostgreSQL 作为主数据库。需要确保数据库服务已启动,并创建了相应的数据库和用户。
|
||||
3. **Redis 服务**:用于缓存和会话管理。需要确保 Redis 服务正在运行。
|
||||
4. **RustFS (S3 兼容对象存储)**:用于存储用户上传的皮肤和头像文件。需要配置一个 S3 兼容的对象存储服务。
|
||||
5. **swag 工具**:用于生成 Swagger API 文档。如果未安装,`run.sh` 脚本会自动安装。
|
||||
|
||||
对于生产环境,建议使用 Docker 或 Kubernetes 来管理这些依赖服务,以确保环境的一致性和可移植性。
|
||||
|
||||
**Section sources**
|
||||
- [go.mod](file://go.mod)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
|
||||
## 部署流程
|
||||
CarrotSkin 提供了多种部署方式,从简单的本地部署到复杂的生产环境部署。
|
||||
|
||||
### 基础部署步骤
|
||||
1. **克隆代码库**:将 CarrotSkin 后端代码克隆到目标服务器。
|
||||
2. **安装依赖**:运行 `go mod tidy` 安装所有 Go 依赖包。
|
||||
3. **配置环境变量**:根据 `start.sh` 脚本中的示例,设置所有必需的环境变量。
|
||||
4. **生成 API 文档**:运行 `swag init` 生成 Swagger API 文档。
|
||||
5. **启动服务**:执行 `run.sh` 或 `start.sh` 脚本启动服务。
|
||||
|
||||
### 使用 run.sh 脚本
|
||||
`run.sh` 脚本是推荐的开发和部署方式,它自动化了大部分部署步骤:
|
||||
- 自动检查并安装 `swag` 工具
|
||||
- 生成最新的 Swagger API 文档
|
||||
- 启动 Go 服务
|
||||
|
||||
```bash
|
||||
./run.sh
|
||||
```
|
||||
|
||||
### 使用 start.sh 脚本
|
||||
`start.sh` 脚本直接设置了所有环境变量并启动服务,适合在环境变量管理不便的场景下使用。
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**Section sources**
|
||||
- [run.sh](file://run.sh)
|
||||
- [start.sh](file://start.sh)
|
||||
- [scripts/dev.sh](file://scripts/dev.sh)
|
||||
|
||||
## 启动脚本详解
|
||||
CarrotSkin 提供了多个启动脚本以适应不同的使用场景。
|
||||
|
||||
### start.sh 脚本
|
||||
`start.sh` 脚本是一个自包含的启动脚本,它在脚本内部定义了所有必需的环境变量,然后直接启动服务。这种方式简单直接,但不推荐用于生产环境,因为敏感信息(如数据库密码)直接暴露在脚本中。
|
||||
|
||||
**Section sources**
|
||||
- [start.sh](file://start.sh)
|
||||
|
||||
### run.sh 脚本
|
||||
`run.sh` 脚本是一个更智能的启动脚本,它分为三个阶段:
|
||||
1. **检查 swag 工具**:确保 `swag` 命令可用,如果不存在则自动安装。
|
||||
2. **生成 Swagger 文档**:运行 `swag init` 命令生成 API 文档,确保 API 文档与代码同步。
|
||||
3. **启动服务器**:最后启动 Go 服务。
|
||||
|
||||
这个脚本的优势在于它确保了 API 文档的实时性,并且不包含敏感的环境变量。
|
||||
|
||||
**Section sources**
|
||||
- [run.sh](file://run.sh)
|
||||
|
||||
### check-env.sh 脚本
|
||||
`scripts/check-env.sh` 是一个环境检查脚本,用于验证必需的环境变量是否已正确设置。它会检查 `.env` 文件是否存在,并验证关键变量(如数据库连接、RustFS 凭证、JWT 密钥)是否已配置。此外,它还会检查 JWT 密钥的长度和是否使用了默认值,提供安全建议。
|
||||
|
||||
**Section sources**
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
|
||||
## 生产环境配置
|
||||
在生产环境中部署 CarrotSkin 时,必须进行严格的安全和性能配置。
|
||||
|
||||
### 环境变量配置
|
||||
所有配置都通过环境变量进行。关键的环境变量包括:
|
||||
|
||||
- **数据库配置**:`DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`
|
||||
- **Redis 配置**:`REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`
|
||||
- **对象存储配置**:`RUSTFS_ENDPOINT`, `RUSTFS_ACCESS_KEY`, `RUSTFS_SECRET_KEY`
|
||||
- **安全配置**:`JWT_SECRET`(必须使用至少 32 位的强随机字符串)
|
||||
|
||||
### 配置管理
|
||||
项目使用 Viper 库从环境变量中读取配置。`pkg/config/config.go` 文件定义了所有配置项的结构和默认值。生产环境中应避免使用脚本中硬编码的默认值,而是通过环境变量或配置管理工具(如 Kubernetes ConfigMap/Secret)来设置。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[环境变量] --> B[pkg/config/config.go]
|
||||
B --> C[应用配置]
|
||||
C --> D[数据库连接]
|
||||
C --> E[Redis连接]
|
||||
C --> F[RustFS连接]
|
||||
C --> G[JWT配置]
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
|
||||
**Section sources**
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [start.sh](file://start.sh)
|
||||
|
||||
## 健康检查与监控
|
||||
CarrotSkin 内置了健康检查功能,便于集成到监控系统中。
|
||||
|
||||
### 健康检查端点
|
||||
服务提供了一个 `/health` 的健康检查端点,返回简单的 JSON 响应:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "CarrotSkin API is running"
|
||||
}
|
||||
```
|
||||
该端点由 `internal/handler/swagger.go` 中的 `HealthCheck` 函数实现,无需认证即可访问,适合用于负载均衡器和容器编排平台的健康探测。
|
||||
|
||||
### 监控建议
|
||||
建议将以下指标纳入监控系统:
|
||||
- HTTP 请求延迟和错误率
|
||||
- 数据库连接池使用情况
|
||||
- Redis 内存使用和命中率
|
||||
- 对象存储的读写性能
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 监控系统
|
||||
participant CarrotSkin服务
|
||||
participant 数据库
|
||||
participant Redis
|
||||
participant 对象存储
|
||||
监控系统->>CarrotSkin服务 : GET /health
|
||||
CarrotSkin服务->>数据库 : 检查连接
|
||||
CarrotSkin服务->>Redis : 检查连接
|
||||
CarrotSkin服务->>对象存储 : 检查连接
|
||||
CarrotSkin服务-->>监控系统 : 返回健康状态
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
|
||||
- [internal/handler/routes.go](file://internal/handler/routes.go)
|
||||
|
||||
**Section sources**
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
|
||||
|
||||
## 日志管理
|
||||
CarrotSkin 使用 Zap 日志库进行日志记录,提供了结构化日志输出。
|
||||
|
||||
### 日志配置
|
||||
日志行为由以下环境变量控制:
|
||||
- `LOG_LEVEL`:日志级别(debug, info, warn, error)
|
||||
- `LOG_FORMAT`:日志格式(json 或 console)
|
||||
- `LOG_OUTPUT`:日志输出位置(文件路径或 stdout)
|
||||
|
||||
### 日志轮转
|
||||
日志文件支持自动轮转,配置包括最大文件大小、备份数量和最大保留天数。日志文件默认存储在 `logs/app.log`。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[应用事件] --> B[pkg/logger/logger.go]
|
||||
B --> C{日志级别}
|
||||
C --> |高于配置级别| D[忽略]
|
||||
C --> |低于或等于| E[格式化]
|
||||
E --> F{输出目标}
|
||||
F --> |stdout| G[控制台输出]
|
||||
F --> |文件| H[写入日志文件]
|
||||
H --> I[日志轮转]
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
|
||||
|
||||
**Section sources**
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
|
||||
## 高级部署策略
|
||||
对于生产环境,建议采用以下高级部署策略。
|
||||
|
||||
### Docker 部署
|
||||
虽然项目未提供 Dockerfile,但可以轻松创建。Docker 部署的优势包括环境隔离、版本控制和易于扩展。
|
||||
|
||||
```dockerfile
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN go mod tidy
|
||||
RUN go build -o carrotskin cmd/server/main.go
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /root/
|
||||
COPY --from=builder /app/carrotskin .
|
||||
COPY --from=builder /app/docs docs
|
||||
EXPOSE 8080
|
||||
CMD ["./carrotskin"]
|
||||
```
|
||||
|
||||
### CI/CD 集成
|
||||
建议将部署流程集成到 CI/CD 管道中:
|
||||
1. 代码推送触发 CI 构建
|
||||
2. 运行单元测试和集成测试
|
||||
3. 构建 Docker 镜像
|
||||
4. 推送镜像到镜像仓库
|
||||
5. 在目标环境部署新版本
|
||||
|
||||
### 高可用性部署
|
||||
对于高可用性要求,建议:
|
||||
- 使用 Kubernetes 部署多个服务实例
|
||||
- 配置负载均衡器分发流量
|
||||
- 使用外部的 PostgreSQL 集群和 Redis 集群
|
||||
- 实现蓝绿部署或金丝雀发布
|
||||
|
||||
## 故障排除
|
||||
### 常见问题
|
||||
- **服务无法启动**:检查 `check-env.sh` 脚本,确保所有必需的环境变量已设置。
|
||||
- **数据库连接失败**:验证数据库主机、端口、用户名和密码是否正确。
|
||||
- **JWT 密钥警告**:生产环境中必须更改默认的 JWT 密钥。
|
||||
- **Swagger 文档缺失**:确保已运行 `swag init` 命令。
|
||||
|
||||
### 调试建议
|
||||
- 查看日志文件 `logs/app.log` 获取详细错误信息
|
||||
- 使用 `curl http://localhost:8080/health` 检查服务健康状态
|
||||
- 检查依赖服务(数据库、Redis、对象存储)是否正常运行
|
||||
|
||||
**Section sources**
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
|
||||
- [internal/handler/swagger.go](file://internal/handler/swagger.go)
|
||||
@@ -1,331 +0,0 @@
|
||||
# JWT配置
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go)
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件聚焦于CarrotSkin项目的JWT认证令牌配置,系统性说明以下内容:
|
||||
- jwt.secret与expire_hours两个核心配置项的作用与默认值
|
||||
- 如何通过环境变量JWT_SECRET与JWT_EXPIRE_HOURS进行安全覆盖
|
||||
- 基于JWTConfig结构体与认证服务代码的安全最佳实践
|
||||
- 密钥生成建议与有效期设置策略
|
||||
|
||||
## 项目结构
|
||||
围绕JWT配置的相关文件分布如下:
|
||||
- 配置加载与默认值:pkg/config/config.go、pkg/config/manager.go
|
||||
- JWT服务与管理器:pkg/auth/jwt.go、pkg/auth/manager.go
|
||||
- 认证处理器与服务层集成:internal/handler/auth_handler.go、internal/service/user_service.go
|
||||
- 环境变量检查脚本:scripts/check-env.sh
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置层"
|
||||
CFG["pkg/config/config.go<br/>加载与默认值"]
|
||||
CMGR["pkg/config/manager.go<br/>配置单例管理"]
|
||||
end
|
||||
subgraph "认证层"
|
||||
JCFG["pkg/auth/jwt.go<br/>JWT服务与声明"]
|
||||
JMGR["pkg/auth/manager.go<br/>JWT服务单例管理"]
|
||||
end
|
||||
subgraph "业务层"
|
||||
AH["internal/handler/auth_handler.go<br/>登录/注册接口"]
|
||||
US["internal/service/user_service.go<br/>登录/注册业务"]
|
||||
end
|
||||
subgraph "工具"
|
||||
ENV["scripts/check-env.sh<br/>环境变量检查"]
|
||||
end
|
||||
CFG --> CMGR
|
||||
CMGR --> JMGR
|
||||
JCFG --> JMGR
|
||||
AH --> US
|
||||
AH --> JMGR
|
||||
US --> JCFG
|
||||
ENV --> CFG
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L1-L78)
|
||||
|
||||
## 核心组件
|
||||
- JWTConfig结构体
|
||||
- 字段:Secret(密钥)、ExpireHours(过期小时数)
|
||||
- 默认值:ExpireHours默认168小时(7天)
|
||||
- 环境变量映射:JWT_SECRET、JWT_EXPIRE_HOURS
|
||||
- JWTService
|
||||
- 作用:生成与验证JWT令牌;声明包含用户ID、用户名、角色及标准声明(签发时间、过期时间等)
|
||||
- 过期时间由expireHours决定,默认168小时
|
||||
- JWT服务管理器
|
||||
- 提供Init(cfg)一次性初始化,GetJWTService()/MustGetJWTService()获取全局实例
|
||||
- 认证服务集成
|
||||
- 登录/注册成功后,通过JWTService生成令牌并返回给客户端
|
||||
- 环境变量检查
|
||||
- 脚本会校验JWT_SECRET长度与默认值提示
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L67-L71)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L58-L77)
|
||||
|
||||
## 架构总览
|
||||
下图展示JWT配置从环境变量到服务使用的端到端流程。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ENV as "环境变量"
|
||||
participant CFG as "配置加载(config.Load)"
|
||||
participant CMGR as "配置管理器(config.Init)"
|
||||
participant JMGR as "JWT管理器(auth.Init)"
|
||||
participant Jsvc as "JWT服务(auth.JWTService)"
|
||||
participant H as "认证处理器(handler.Login/Register)"
|
||||
participant S as "业务服务(service.LoginUser/RegisterUser)"
|
||||
ENV-->>CFG : "JWT_SECRET/JWT_EXPIRE_HOURS"
|
||||
CFG-->>CMGR : "加载并解析配置(Config)"
|
||||
CMGR-->>JMGR : "传入JWTConfig(Secret, ExpireHours)"
|
||||
JMGR-->>Jsvc : "创建JWTService实例"
|
||||
H->>S : "调用登录/注册业务"
|
||||
S->>Jsvc : "生成JWT令牌"
|
||||
Jsvc-->>S : "返回token"
|
||||
S-->>H : "返回token与用户信息"
|
||||
H-->>Client : "HTTP响应(含token)"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L32-L53)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### JWTConfig与默认值
|
||||
- 结构体定义
|
||||
- Secret:字符串,用于签名与验证JWT
|
||||
- ExpireHours:整数,令牌有效期(小时),默认168(7天)
|
||||
- 默认值来源
|
||||
- 通过Viper设置默认值,确保未提供环境变量时仍能运行
|
||||
- 环境变量映射
|
||||
- JWT_SECRET → jwt.secret
|
||||
- JWT_EXPIRE_HOURS → jwt.expire_hours
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L67-L71)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)
|
||||
|
||||
### JWTService与Claims
|
||||
- JWTService
|
||||
- 字段:secretKey、expireHours
|
||||
- 方法:GenerateToken、ValidateToken
|
||||
- Claims
|
||||
- 包含用户ID、用户名、角色以及标准声明(签发时间、过期时间、生效时间、发行者等)
|
||||
- 令牌生成
|
||||
- 使用HS256算法签名,过期时间基于expireHours计算
|
||||
- 令牌验证
|
||||
- 使用同一secretKey进行验证,若签名不符或过期则报错
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class JWTService {
|
||||
-string secretKey
|
||||
-int expireHours
|
||||
+GenerateToken(userID, username, role) string,error
|
||||
+ValidateToken(tokenString) Claims,error
|
||||
}
|
||||
class Claims {
|
||||
+int64 UserID
|
||||
+string Username
|
||||
+string Role
|
||||
+RegisteredClaims
|
||||
}
|
||||
JWTService --> Claims : "生成/验证"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
|
||||
|
||||
章节来源
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L10-L71)
|
||||
|
||||
### JWT服务管理器
|
||||
- Init(cfg)
|
||||
- 仅初始化一次,使用sync.Once保证幂等
|
||||
- 将cfg中的Secret与ExpireHours注入JWTService
|
||||
- GetJWTService/MustGetJWTService
|
||||
- 提供全局唯一实例访问;未初始化时报错或panic
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["调用 auth.Init(cfg)"]) --> Once{"是否已初始化?"}
|
||||
Once --> |否| Create["创建JWTService实例<br/>注入Secret/ExpireHours"]
|
||||
Once --> |是| Skip["跳过初始化"]
|
||||
Create --> Done(["完成"])
|
||||
Skip --> Done
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
|
||||
|
||||
章节来源
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L41)
|
||||
|
||||
### 认证服务与JWT集成
|
||||
- 登录/注册成功后,业务层调用JWTService生成令牌
|
||||
- 处理器层接收令牌并返回给客户端
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as "客户端"
|
||||
participant H as "Handler(Login/Register)"
|
||||
participant S as "Service(LoginUser/RegisterUser)"
|
||||
participant J as "JWTService"
|
||||
C->>H : "POST /api/v1/auth/login"
|
||||
H->>S : "LoginUser(jwtService, ...)"
|
||||
S->>J : "GenerateToken(userID, username, role)"
|
||||
J-->>S : "token"
|
||||
S-->>H : "user + token"
|
||||
H-->>C : "200 OK + token"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L32-L53)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L32-L53)
|
||||
|
||||
### 环境变量覆盖与安全检查
|
||||
- 环境变量映射
|
||||
- JWT_SECRET → jwt.secret
|
||||
- JWT_EXPIRE_HOURS → jwt.expire_hours
|
||||
- 默认值
|
||||
- ExpireHours默认168(7天)
|
||||
- 安全检查
|
||||
- 脚本会检查JWT_SECRET长度是否小于32字符,并提示使用默认密钥的风险
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L58-L77)
|
||||
|
||||
## 依赖关系分析
|
||||
- 配置层
|
||||
- config.Load负责从.env与环境变量加载配置,设置默认值并映射JWT相关键
|
||||
- config.Init提供全局配置单例
|
||||
- 认证层
|
||||
- auth.Init接收JWTConfig,创建JWTService实例
|
||||
- auth.GetJWTService/MustGetJWTService提供全局访问
|
||||
- 业务层
|
||||
- handler.Login/Register通过MustGetJWTService获取JWTService
|
||||
- service.LoginUser/RegisterUser调用JWTService生成令牌
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
CFG["config.Load"] --> CMGR["config.Init/GetConfig"]
|
||||
CMGR --> JMGR["auth.Init"]
|
||||
JMGR --> Jsvc["auth.JWTService"]
|
||||
AH["handler.Login/Register"] --> Jsvc
|
||||
US["service.LoginUser/RegisterUser"] --> Jsvc
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L18-L23)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L97-L147)
|
||||
- [internal/service/user_service.go](file://internal/service/user_service.go#L55-L107)
|
||||
|
||||
## 性能考量
|
||||
- 令牌生成与验证均为内存操作,开销极低
|
||||
- 过期时间越长,客户端缓存令牌的时间越久,减少鉴权次数;但会增加泄露风险
|
||||
- 建议在移动端或易泄露设备上缩短expire_hours,在可信环境适当延长
|
||||
|
||||
## 故障排查指南
|
||||
- JWT服务未初始化
|
||||
- 现象:调用GetJWTService/MustGetJWTService时报错或panic
|
||||
- 排查:确认已在应用启动阶段调用auth.Init(cfg)
|
||||
- 令牌无效
|
||||
- 现象:ValidateToken返回错误
|
||||
- 排查:确认使用相同的JWT_SECRET;检查网络传输是否篡改;核对过期时间
|
||||
- 环境变量未生效
|
||||
- 现象:JWT_EXPIRE_HOURS未按预期
|
||||
- 排查:确认环境变量名正确且已导出;检查.env文件路径与加载顺序
|
||||
|
||||
章节来源
|
||||
- [pkg/auth/manager.go](file://pkg/auth/manager.go#L26-L41)
|
||||
- [pkg/auth/jwt.go](file://pkg/auth/jwt.go#L55-L71)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
|
||||
## 结论
|
||||
- jwt.secret用于签名与验证JWT,必须保密且足够随机
|
||||
- expire_hours控制令牌有效期,默认168小时(7天),可通过JWT_EXPIRE_HOURS覆盖
|
||||
- 通过JWT_SECRET与JWT_EXPIRE_HOURS实现安全可控的令牌策略
|
||||
- 建议在生产环境使用长随机密钥,并根据业务场景调整有效期
|
||||
|
||||
## 附录
|
||||
|
||||
### 安全最佳实践
|
||||
- 密钥生成建议
|
||||
- 使用足够长度的随机字符串作为JWT_SECRET(建议≥32字符)
|
||||
- 避免使用默认密钥或弱口令
|
||||
- 有效期设置策略
|
||||
- 移动端/公共设备:较短有效期(如24-72小时)
|
||||
- 可信服务间:可适当延长(如7天)
|
||||
- 环境变量管理
|
||||
- 在部署时通过环境变量覆盖,避免硬编码
|
||||
- 使用脚本检查密钥长度与默认值风险
|
||||
|
||||
章节来源
|
||||
- [scripts/check-env.sh](file://scripts/check-env.sh#L58-L77)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L163-L165)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L220-L223)
|
||||
@@ -1,331 +0,0 @@
|
||||
# Redis配置
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向CarrotSkin项目的开发者,系统性梳理Redis缓存系统的配置与使用方式,重点说明以下内容:
|
||||
- RedisConfig结构体中的各配置项(host、port、password、database、pool_size)的作用与默认值
|
||||
- pool_size参数对并发性能的影响机制
|
||||
- 如何通过REDIS_*环境变量动态覆盖配置
|
||||
- 结合RedisConfig结构体与连接初始化逻辑,给出多环境部署建议与连接池大小调优方法
|
||||
|
||||
## 项目结构
|
||||
Redis配置与使用涉及如下关键模块:
|
||||
- 配置加载与默认值:pkg/config
|
||||
- Redis客户端封装与连接初始化:pkg/redis
|
||||
- 应用启动流程:cmd/server/main.go
|
||||
- 业务使用示例:internal/handler与internal/service
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置层"
|
||||
CFG["pkg/config/config.go<br/>RedisConfig结构体与默认值"]
|
||||
CM["pkg/config/manager.go<br/>配置单例管理"]
|
||||
end
|
||||
subgraph "Redis层"
|
||||
RC["pkg/redis/redis.go<br/>Client封装与New初始化"]
|
||||
RM["pkg/redis/manager.go<br/>Init/GetClient/MustGetClient"]
|
||||
end
|
||||
subgraph "应用层"
|
||||
MAIN["cmd/server/main.go<br/>启动时初始化Redis"]
|
||||
AUTH["internal/handler/auth_handler.go<br/>使用MustGetClient()"]
|
||||
SVC["internal/service/verification_service.go<br/>验证码场景使用Redis"]
|
||||
end
|
||||
CFG --> CM
|
||||
CM --> MAIN
|
||||
MAIN --> RM
|
||||
RM --> RC
|
||||
AUTH --> RM
|
||||
SVC --> RC
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L52-L61)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L52-L61)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
|
||||
## 核心组件
|
||||
- RedisConfig结构体
|
||||
- 字段:host、port、password、database、pool_size
|
||||
- 默认值:由配置加载器在未显式提供时设置
|
||||
- 配置加载与环境变量覆盖
|
||||
- 默认值设置、环境变量前缀、映射与覆盖逻辑
|
||||
- Redis客户端封装与初始化
|
||||
- Client封装、New初始化、Ping连通性校验、连接池大小
|
||||
- 单例初始化与获取接口(Init/GetClient/MustGetClient)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
|
||||
## 架构总览
|
||||
下图展示Redis配置与初始化在应用生命周期中的位置与交互。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Main as "main.go"
|
||||
participant CfgMgr as "config.Manager"
|
||||
participant Cfg as "config.Config"
|
||||
participant RedisMgr as "redis.Manager"
|
||||
participant RedisClient as "redis.Client"
|
||||
Main->>CfgMgr : Init()
|
||||
CfgMgr-->>Main : 配置实例
|
||||
Main->>RedisMgr : Init(RedisConfig, Logger)
|
||||
RedisMgr->>RedisClient : New(cfg, logger)
|
||||
RedisClient->>RedisClient : Ping()连通性校验
|
||||
RedisMgr-->>Main : 初始化完成
|
||||
Main-->>Main : 启动HTTP服务
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L61)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### RedisConfig结构体与默认值
|
||||
- 字段定义与含义
|
||||
- host:Redis服务器地址,默认值由配置加载器设置
|
||||
- port:Redis服务器端口,默认值由配置加载器设置
|
||||
- password:Redis认证密码,默认值由配置加载器设置
|
||||
- database:Redis数据库索引,默认值由配置加载器设置
|
||||
- pool_size:Redis连接池大小,默认值由配置加载器设置
|
||||
- 默认值来源
|
||||
- 配置加载器在未显式提供时设置默认值,确保应用在最小配置下仍可运行
|
||||
- 环境变量映射
|
||||
- REDIS_HOST、REDIS_PORT、REDIS_PASSWORD、REDIS_DATABASE
|
||||
- REDIS_POOL_SIZE用于覆盖连接池大小
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
|
||||
|
||||
### 连接初始化与Ping校验
|
||||
- 初始化流程
|
||||
- 通过redis.NewClient创建客户端,使用RedisConfig中的host/port/password/database/pool_size
|
||||
- 使用Ping进行连通性校验,失败则返回错误
|
||||
- 初始化成功后记录日志,包含host/port/database
|
||||
- 并发与线程安全
|
||||
- Redis初始化采用once.Do保证全局仅初始化一次
|
||||
- GetClient/MustGetClient提供线程安全的获取方式
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["进入Init(cfg, logger)"]) --> Once["once.Do执行"]
|
||||
Once --> NewClient["创建redis.Client(options)"]
|
||||
NewClient --> Ping["Ping()连通性校验"]
|
||||
Ping --> Ok{"Ping成功?"}
|
||||
Ok --> |否| Err["返回错误"]
|
||||
Ok --> |是| Log["记录连接成功日志"]
|
||||
Log --> Done(["返回Client实例"])
|
||||
Err --> Done
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
|
||||
|
||||
章节来源
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
|
||||
|
||||
### 环境变量动态覆盖机制
|
||||
- 环境变量前缀与映射
|
||||
- 配置加载器设置环境变量前缀为CARROTSKIN,并将redis.*字段绑定到REDIS_*环境变量
|
||||
- 动态覆盖逻辑
|
||||
- 在解析默认值与环境变量后,额外处理REDIS_POOL_SIZE以覆盖连接池大小
|
||||
- 使用建议
|
||||
- 开发环境可使用较小pool_size;生产环境根据QPS与并发峰值评估后设置
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
|
||||
|
||||
### 业务使用示例
|
||||
- 验证码场景
|
||||
- 发送验证码前检查频率限制键是否存在
|
||||
- 将验证码写入Redis并设置过期时间
|
||||
- 发送邮件失败时删除验证码键
|
||||
- 登录/注册/重置密码
|
||||
- Handler层通过MustGetClient()获取Redis客户端
|
||||
- Service层调用Redis进行验证码校验与清理
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as "auth_handler"
|
||||
participant S as "verification_service"
|
||||
participant R as "redis.Client"
|
||||
H->>R : MustGetClient()
|
||||
H->>S : SendVerificationCode(ctx, R, emailService, email, type)
|
||||
S->>R : Exists(rateLimitKey)
|
||||
alt 存在频率限制
|
||||
S-->>H : 返回“发送过于频繁”
|
||||
else 无频率限制
|
||||
S->>R : Set(codeKey, code, expiration)
|
||||
S->>R : Set(rateLimitKey, "1", rateLimit)
|
||||
S-->>H : 发送成功
|
||||
end
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L31-L46)
|
||||
|
||||
章节来源
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
|
||||
## 依赖关系分析
|
||||
- 配置到Redis的依赖链
|
||||
- config.Manager -> config.Config -> redis.Manager -> redis.Client
|
||||
- 启动阶段依赖
|
||||
- main.go在启动时顺序初始化配置、日志、数据库、JWT、Redis、对象存储、邮件服务
|
||||
- 业务依赖
|
||||
- Handler与Service通过redis.Manager提供的单例接口使用Redis
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
CFG["config.Config"] --> RM["redis.Manager"]
|
||||
RM --> RC["redis.Client"]
|
||||
MAIN["cmd/server/main.go"] --> RM
|
||||
AUTH["internal/handler/auth_handler.go"] --> RM
|
||||
SVC["internal/service/verification_service.go"] --> RC
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L61)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
|
||||
章节来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L61)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L46)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L20-L46)
|
||||
- [internal/handler/auth_handler.go](file://internal/handler/auth_handler.go#L27-L33)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
|
||||
## 性能考量
|
||||
- pool_size对并发性能的影响机制
|
||||
- 连接池大小决定同时可用的底层TCP连接数
|
||||
- 在高并发场景下,若pool_size过小,可能出现连接争用与等待,导致延迟上升
|
||||
- 若pool_size过大,会增加Redis端的连接管理开销与资源占用
|
||||
- 调优建议
|
||||
- 基准测试:在预生产环境模拟峰值QPS,逐步提升pool_size并观察延迟与错误率
|
||||
- 观察指标:监控Redis连接数、等待队列长度、命令执行延迟
|
||||
- 平衡策略:在满足延迟目标的前提下,尽量保持pool_size适中,避免过度放大
|
||||
- 与业务场景的匹配
|
||||
- 验证码发送/校验属于短时高频写入场景,适度增大pool_size有助于降低写入抖动
|
||||
- 读多写少的场景可结合Pipeline/TxPipeline减少RTT,进一步提升吞吐
|
||||
|
||||
[本节为通用性能讨论,不直接分析具体文件]
|
||||
|
||||
## 故障排查指南
|
||||
- 初始化失败
|
||||
- 症状:启动时报Redis连接失败
|
||||
- 排查要点:确认REDIS_HOST/REDIS_PORT/REDIS_PASSWORD/REDIS_DATABASE是否正确;检查网络连通性;确认Redis服务状态
|
||||
- 参考实现:初始化时进行Ping校验,失败返回错误
|
||||
- 未初始化即使用
|
||||
- 症状:调用MustGetClient()时panic或GetClient()返回错误
|
||||
- 排查要点:确保在main中先调用redis.Init(),再启动HTTP服务
|
||||
- 频繁超时或错误
|
||||
- 症状:业务调用Redis出现超时或错误
|
||||
- 排查要点:检查pool_size是否过小;确认Redis资源瓶颈;优化业务逻辑减少阻塞操作
|
||||
- 验证码发送过于频繁
|
||||
- 症状:提示发送过于频繁
|
||||
- 排查要点:检查频率限制键是否存在且未过期;确认业务逻辑正确清理
|
||||
|
||||
章节来源
|
||||
- [pkg/redis/redis.go](file://pkg/redis/redis.go#L21-L52)
|
||||
- [pkg/redis/manager.go](file://pkg/redis/manager.go#L31-L46)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L52-L61)
|
||||
- [internal/service/verification_service.go](file://internal/service/verification_service.go#L40-L77)
|
||||
|
||||
## 结论
|
||||
- RedisConfig提供了简洁明确的配置项,配合默认值与环境变量覆盖,可在多环境中快速部署
|
||||
- pool_size直接影响并发性能,应结合业务负载与Redis资源进行调优
|
||||
- 通过once.Do保证Redis客户端单例初始化,配合MustGetClient/GetClient简化了业务侧使用
|
||||
- 建议在生产环境启用合理的pool_size,并持续监控连接池利用率与延迟指标
|
||||
|
||||
[本节为总结性内容,不直接分析具体文件]
|
||||
|
||||
## 附录
|
||||
|
||||
### 配置项与默认值对照表
|
||||
- host:Redis主机地址,默认值由配置加载器设置
|
||||
- port:Redis端口,默认值由配置加载器设置
|
||||
- password:Redis认证密码,默认值由配置加载器设置
|
||||
- database:Redis数据库索引,默认值由配置加载器设置
|
||||
- pool_size:Redis连接池大小,默认值由配置加载器设置
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L49-L56)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
|
||||
### 环境变量覆盖清单
|
||||
- REDIS_HOST:覆盖host
|
||||
- REDIS_PORT:覆盖port
|
||||
- REDIS_PASSWORD:覆盖password
|
||||
- REDIS_DATABASE:覆盖database
|
||||
- REDIS_POOL_SIZE:覆盖pool_size
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L304)
|
||||
|
||||
### 多环境部署建议
|
||||
- 开发环境
|
||||
- 使用较小pool_size(如默认值),便于本地调试
|
||||
- 通过REDIS_*环境变量快速切换Redis实例
|
||||
- 预生产/生产环境
|
||||
- 基于基准测试确定pool_size,结合监控指标持续优化
|
||||
- 使用独立database索引隔离不同环境数据
|
||||
- 对password进行严格管理,避免明文配置
|
||||
|
||||
[本节为通用建议,不直接分析具体文件]
|
||||
@@ -1,305 +0,0 @@
|
||||
# 对象存储配置
|
||||
|
||||
<cite>
|
||||
**本文引用的文件列表**
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go)
|
||||
- [start.sh](file://start.sh)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向开发者,系统性说明 CarrotSkin 项目与 RustFS(S3 兼容)对象存储的集成配置。重点涵盖以下方面:
|
||||
- 核心配置项 rustfs.endpoint、access_key、secret_key、use_ssl 的作用与含义
|
||||
- 存储桶配置 buckets 的动态加载机制,以及通过 RUSTFS_BUCKET_TEXTURES 和 RUSTFS_BUCKET_AVATARS 环境变量为不同用途(材质与头像)设置存储桶
|
||||
- 基于 RustFSConfig 结构体的安全配置最佳实践,包括凭证管理与 SSL 配置建议
|
||||
|
||||
## 项目结构
|
||||
CarrotSkin 的对象存储配置由“配置层 → 存储层 → 业务层”三层协作完成:
|
||||
- 配置层:负责从环境变量加载并合并 RustFSConfig,支持运行时覆盖
|
||||
- 存储层:封装 S3 兼容客户端(minio-go),提供桶名映射与预签名上传能力
|
||||
- 业务层:根据文件类型生成上传 URL,按需选择对应存储桶
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置层"
|
||||
CFG["RustFSConfig<br/>endpoint/access_key/secret_key/use_ssl/buckets"]
|
||||
CMGR["配置管理器<br/>Init/GetRustFSConfig"]
|
||||
end
|
||||
subgraph "存储层"
|
||||
SMGR["存储管理器<br/>Init/GetClient"]
|
||||
SCLI["StorageClient<br/>minio.Client + buckets 映射"]
|
||||
end
|
||||
subgraph "业务层"
|
||||
SVC["上传服务<br/>生成头像/材质上传URL"]
|
||||
end
|
||||
CMGR --> CFG
|
||||
SMGR --> SCLI
|
||||
SVC --> SCLI
|
||||
CFG --> SMGR
|
||||
SCLI --> SVC
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L63-L69)
|
||||
|
||||
## 核心组件
|
||||
- RustFSConfig:定义对象存储的端点、凭证、协议开关与桶映射
|
||||
- 配置管理器:提供全局单例的配置加载与获取能力
|
||||
- 存储管理器:提供全局单例的存储客户端初始化与获取能力
|
||||
- StorageClient:封装 minio-go 客户端,提供桶名映射与预签名上传能力
|
||||
- 上传服务:按文件类型生成预签名上传 URL,并选择对应存储桶
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
|
||||
## 架构总览
|
||||
下图展示从应用启动到生成上传 URL 的关键流程,以及各组件之间的依赖关系。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Main as "主程序"
|
||||
participant CfgMgr as "配置管理器"
|
||||
participant Cfg as "RustFSConfig"
|
||||
participant StorMgr as "存储管理器"
|
||||
participant StorCli as "StorageClient"
|
||||
participant Svc as "上传服务"
|
||||
Main->>CfgMgr : 调用 Init()
|
||||
CfgMgr->>CfgMgr : 加载环境变量并合并默认值
|
||||
CfgMgr-->>Main : 返回全局 RustFSConfig
|
||||
Main->>StorMgr : 调用 Init(RustFSConfig)
|
||||
StorMgr->>StorCli : NewStorage(cfg)
|
||||
StorCli-->>StorMgr : 返回 StorageClient
|
||||
StorMgr-->>Main : 初始化完成
|
||||
Svc->>StorCli : GetBucket("avatars"/"textures")
|
||||
Svc->>StorCli : GeneratePresignedPostURL(...)
|
||||
StorCli-->>Svc : 返回 PostURL + FormData + FileURL
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L70)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### RustFSConfig 结构体与配置项说明
|
||||
- endpoint:RustFS 服务地址(含端口)。用于构建预签名上传 URL 的最终访问地址与协议选择
|
||||
- access_key / secret_key:访问凭证,用于构造静态凭据并建立 S3 兼容客户端
|
||||
- use_ssl:是否启用 HTTPS 协议;影响预签名上传 URL 的协议与文件访问 URL 的协议
|
||||
- buckets:存储桶映射,键为用途标识(如 "textures"、"avatars"),值为实际桶名
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L58-L66)
|
||||
|
||||
### 配置加载与动态覆盖
|
||||
- 默认值:通过 viper 设置默认值,包含 endpoint 与 use_ssl 的默认值
|
||||
- 环境变量映射:将 CARROTSKIN 前缀的环境变量绑定到配置键
|
||||
- 动态覆盖:overrideFromEnv 支持通过 RUSTFS_BUCKET_TEXTURES 与 RUSTFS_BUCKET_AVATARS 动态注入存储桶映射,无需修改配置文件
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
|
||||
|
||||
### 存储客户端初始化与连接测试
|
||||
- 客户端创建:基于 endpoint、access_key、secret_key、use_ssl 构造 minio-go 客户端
|
||||
- 连接测试:当凭证非空时,尝试列举桶以验证连通性
|
||||
- 桶映射:将 RustFSConfig 中的 buckets 直接注入 StorageClient
|
||||
|
||||
章节来源
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L49)
|
||||
|
||||
### 存储桶映射与上传 URL 生成
|
||||
- GetBucket:按用途键("textures" 或 "avatars")从映射中取桶名
|
||||
- 预签名上传:根据文件类型与大小范围生成 POST 策略,返回 PostURL、FormData 与最终 FileURL
|
||||
- 协议选择:根据 use_ssl 决定协议(http/https)
|
||||
|
||||
章节来源
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L57-L121)
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L160)
|
||||
|
||||
### 业务层调用链(头像上传)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Svc as "上传服务"
|
||||
participant StorCli as "StorageClient"
|
||||
participant Cfg as "RustFSConfig"
|
||||
Svc->>Svc : 校验文件名与扩展名
|
||||
Svc->>StorCli : GetBucket("avatars")
|
||||
StorCli-->>Svc : 返回桶名
|
||||
Svc->>StorCli : GeneratePresignedPostURL(bucket, objectName, min/max/expire, useSSL, endpoint)
|
||||
StorCli-->>Svc : 返回 PostURL + FormData + FileURL
|
||||
Svc-->>Svc : 组装响应并返回
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L78-L115)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
|
||||
|
||||
### 业务层调用链(材质上传)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Svc as "上传服务"
|
||||
participant StorCli as "StorageClient"
|
||||
participant Cfg as "RustFSConfig"
|
||||
Svc->>Svc : 校验文件名与扩展名
|
||||
Svc->>Svc : 校验材质类型(SKIN/CAPE)
|
||||
Svc->>StorCli : GetBucket("textures")
|
||||
StorCli-->>Svc : 返回桶名
|
||||
Svc->>StorCli : GeneratePresignedPostURL(...)
|
||||
StorCli-->>Svc : 返回 PostURL + FormData + FileURL
|
||||
Svc-->>Svc : 组装响应并返回
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L117-L160)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L121)
|
||||
|
||||
### 存储桶动态加载机制
|
||||
- 环境变量注入:RUSTFS_BUCKET_TEXTURES 与 RUSTFS_BUCKET_AVATARS 分别为“材质”和“头像”用途注入桶名
|
||||
- 映射合并:overrideFromEnv 在 RustFS.Buckets 为空时创建映射,再写入对应键值
|
||||
- 无配置文件依赖:完全通过环境变量驱动,便于容器化部署与多环境切换
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["启动"]) --> LoadEnv["加载环境变量"]
|
||||
LoadEnv --> Override["overrideFromEnv 动态覆盖"]
|
||||
Override --> CheckTextures{"RUSTFS_BUCKET_TEXTURES 是否存在"}
|
||||
CheckTextures --> |是| PutTextures["写入 buckets['textures']"]
|
||||
CheckTextures --> |否| Next1["跳过"]
|
||||
PutTextures --> Next2["继续检查"]
|
||||
Next1 --> Next2
|
||||
Next2 --> CheckAvatars{"RUSTFS_BUCKET_AVATARS 是否存在"}
|
||||
CheckAvatars --> |是| PutAvatars["写入 buckets['avatars']"]
|
||||
CheckAvatars --> |否| End(["结束"])
|
||||
PutAvatars --> End
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
|
||||
|
||||
## 依赖关系分析
|
||||
- 配置层依赖 viper 与 godotenv,负责环境变量加载与默认值设置
|
||||
- 存储层依赖 minio-go,封装客户端与桶映射
|
||||
- 业务层依赖存储层提供的预签名上传能力
|
||||
- 启动流程在 main 中依次初始化配置、日志、数据库、JWT、Redis、对象存储与邮件服务
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Viper["viper/godotenv"] --> Config["RustFSConfig"]
|
||||
Config --> Manager["配置管理器"]
|
||||
Manager --> StorageMgr["存储管理器"]
|
||||
StorageMgr --> StorageClient["StorageClient(minio-go)"]
|
||||
StorageClient --> UploadSvc["上传服务"]
|
||||
Main["main"] --> Manager
|
||||
Main --> StorageMgr
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L70)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L19-L63)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L18-L44)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L121)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L70)
|
||||
|
||||
## 性能考量
|
||||
- 预签名上传避免了服务端中转,降低带宽与延迟
|
||||
- 上传 URL 过期时间短(默认 15 分钟),减少凭证暴露风险
|
||||
- 桶映射为内存查找,开销极低
|
||||
- 连接测试仅在凭证非空时进行,避免不必要的网络往返
|
||||
|
||||
[本节为通用指导,不涉及具体文件分析]
|
||||
|
||||
## 故障排查指南
|
||||
- 配置未初始化
|
||||
- 现象:调用 GetRustFSConfig/MustGetRustFSConfig 或 GetClient/MustGetClient 报错
|
||||
- 排查:确认已在 main 中调用 config.Init() 与 storage.Init()
|
||||
- 凭证为空导致连接测试跳过
|
||||
- 现象:NewStorage 成功但未进行 ListBuckets 测试
|
||||
- 排查:若需验证连通性,请提供 access_key 与 secret_key
|
||||
- 存储桶不存在
|
||||
- 现象:GetBucket 返回错误
|
||||
- 排查:确认 RUSTFS_BUCKET_TEXTURES/RUSTFS_BUCKET_AVATARS 已正确设置,且与 RustFS 实际桶名一致
|
||||
- 预签名上传失败
|
||||
- 现象:GeneratePresignedPostURL 返回错误
|
||||
- 排查:检查 min/max 大小范围、过期时间、use_ssl 与 endpoint 是否匹配
|
||||
|
||||
章节来源
|
||||
- [pkg/config/manager.go](file://pkg/config/manager.go#L31-L63)
|
||||
- [pkg/storage/manager.go](file://pkg/storage/manager.go#L29-L44)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L20-L49)
|
||||
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L57-L121)
|
||||
|
||||
## 结论
|
||||
- CarrotSkin 通过 RustFSConfig 将对象存储配置集中管理,并以环境变量驱动,实现灵活部署
|
||||
- 通过 RUSTFS_BUCKET_TEXTURES 与 RUSTFS_BUCKET_AVATARS 实现“材质/头像”两类资源的桶级隔离
|
||||
- 基于 minio-go 的预签名上传机制,既保证安全性又提升性能
|
||||
- 建议遵循安全最佳实践(最小权限、短期凭证、HTTPS)以保障生产安全
|
||||
|
||||
[本节为总结性内容,不涉及具体文件分析]
|
||||
|
||||
## 附录
|
||||
|
||||
### 环境变量与默认值对照
|
||||
- RUSTFS_ENDPOINT:RustFS 服务地址,默认值由 viper 设置
|
||||
- RUSTFS_ACCESS_KEY:访问密钥
|
||||
- RUSTFS_SECRET_KEY:私有密钥
|
||||
- RUSTFS_USE_SSL:是否启用 HTTPS
|
||||
- RUSTFS_BUCKET_TEXTURES:材质用途桶名
|
||||
- RUSTFS_BUCKET_AVATARS:头像用途桶名
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L236)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L253)
|
||||
- [start.sh](file://start.sh#L22-L27)
|
||||
|
||||
### 启动脚本示例
|
||||
- start.sh 展示了如何设置对象存储相关环境变量,便于本地开发与测试
|
||||
|
||||
章节来源
|
||||
- [start.sh](file://start.sh#L22-L27)
|
||||
@@ -1,364 +0,0 @@
|
||||
# 数据库配置
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go)
|
||||
- [start.sh](file://start.sh)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件聚焦 CarrotSkin 项目的数据库连接与连接池配置,系统性说明以下内容:
|
||||
- 连接参数 database.driver、host、port、username、password、database、ssl_mode、timezone 的用途与默认值来源
|
||||
- 连接池参数 max_idle_conns、max_open_conns、conn_max_lifetime 的工作机制与性能影响
|
||||
- 如何通过 DATABASE_* 环境变量覆盖默认配置
|
||||
- 结合 DatabaseConfig 结构体与数据库初始化流程的最佳实践,包含生产环境安全建议与连接池调优策略
|
||||
|
||||
## 项目结构
|
||||
数据库相关能力由配置层与数据库层协同实现:
|
||||
- 配置层负责加载默认值、绑定环境变量映射,并支持从环境变量覆盖连接池参数
|
||||
- 数据库层负责根据配置构建 DSN、初始化 GORM 连接、设置连接池并进行连通性测试
|
||||
- 应用入口在启动时加载配置、初始化数据库并执行迁移
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "配置层"
|
||||
CFG["pkg/config/config.go<br/>DatabaseConfig/默认值/环境映射"]
|
||||
end
|
||||
subgraph "数据库层"
|
||||
DBM["pkg/database/manager.go<br/>Init/GetDB/AutoMigrate/Close"]
|
||||
PG["pkg/database/postgres.go<br/>New/GetDSN"]
|
||||
end
|
||||
subgraph "应用入口"
|
||||
MAIN["cmd/server/main.go<br/>加载配置/初始化数据库/迁移"]
|
||||
end
|
||||
ENV["环境变量<br/>DATABASE_*"]
|
||||
ENV --> CFG
|
||||
CFG --> DBM
|
||||
DBM --> PG
|
||||
MAIN --> CFG
|
||||
MAIN --> DBM
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
|
||||
|
||||
## 核心组件
|
||||
- DatabaseConfig 结构体:承载数据库连接与连接池配置
|
||||
- 配置加载与覆盖:默认值、环境变量映射、连接池参数覆盖
|
||||
- 数据库初始化:构建 DSN、初始化 GORM、设置连接池、Ping 测试
|
||||
- 应用启动流程:加载配置 → 初始化数据库 → 自动迁移
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
|
||||
|
||||
## 架构总览
|
||||
下图展示从配置到数据库连接的关键调用链路与职责分工。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Entrypoint as "应用入口(main.go)"
|
||||
participant Cfg as "配置(Config.Load)"
|
||||
participant DBMgr as "数据库管理器(database.Init)"
|
||||
participant DB as "数据库(New)"
|
||||
participant SQL as "底层sql.DB"
|
||||
Entrypoint->>Cfg : "加载配置"
|
||||
Cfg-->>Entrypoint : "返回完整配置"
|
||||
Entrypoint->>DBMgr : "Init(DatabaseConfig)"
|
||||
DBMgr->>DB : "New(DatabaseConfig)"
|
||||
DB->>DB : "构造DSN"
|
||||
DB->>SQL : "gorm.Open + 获取*sql.DB"
|
||||
DB->>SQL : "SetMaxIdleConns/SetMaxOpenConns/SetConnMaxLifetime"
|
||||
DB->>SQL : "Ping() 测试连接"
|
||||
DB-->>DBMgr : "*gorm.DB"
|
||||
DBMgr-->>Entrypoint : "初始化完成"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L33)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### DatabaseConfig 字段与用途
|
||||
- database.driver
|
||||
- 作用:驱动类型标识,用于控制日志级别与后续扩展点
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_DRIVER
|
||||
- database.host
|
||||
- 作用:数据库主机地址
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_HOST
|
||||
- database.port
|
||||
- 作用:数据库端口
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_PORT
|
||||
- database.username
|
||||
- 作用:数据库用户名
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_USERNAME
|
||||
- database.password
|
||||
- 作用:数据库密码
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_PASSWORD
|
||||
- database.database
|
||||
- 作用:数据库名
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_NAME
|
||||
- database.ssl_mode
|
||||
- 作用:SSL 模式(如 disable/require 等),影响 TLS 连接
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_SSL_MODE
|
||||
- database.timezone
|
||||
- 作用:时区设置,影响时间字段的时区行为
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量映射:DATABASE_TIMEZONE
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
|
||||
|
||||
### 连接池配置项与工作机制
|
||||
- max_idle_conns
|
||||
- 作用:最大空闲连接数,控制连接池中空闲连接上限
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量覆盖:DATABASE_MAX_IDLE_CONNS
|
||||
- max_open_conns
|
||||
- 作用:最大打开连接数,限制并发连接上限
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量覆盖:DATABASE_MAX_OPEN_CONNS
|
||||
- conn_max_lifetime
|
||||
- 作用:连接最大存活时间,到期后连接会被回收
|
||||
- 默认值:来自默认配置
|
||||
- 环境变量覆盖:DATABASE_CONN_MAX_LIFETIME(支持时长字符串,如“1h”)
|
||||
|
||||
这些参数在 New 中通过底层 sql.DB 设置,并在 Ping 成功后生效。
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L49-L53)
|
||||
|
||||
### 环境变量覆盖与默认值
|
||||
- 默认值来源
|
||||
- 通过 viper.SetDefault 设置,确保即使未提供对应环境变量也能有合理默认
|
||||
- 环境变量映射
|
||||
- 使用 viper.BindEnv 将配置键映射到 DATABASE_* 前缀的环境变量
|
||||
- 连接池参数覆盖
|
||||
- 通过 overrideFromEnv 从环境变量读取整型或时长字符串并覆盖默认值
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
|
||||
|
||||
### 数据库初始化流程与 DSN 构造
|
||||
- DSN 构造
|
||||
- New 中依据 DatabaseConfig 组装 DSN,包含 host、port、user、password、dbname、sslmode、TimeZone
|
||||
- GORM 初始化
|
||||
- 根据 driver 决定日志级别;禁用迁移时的外键约束以避免循环依赖
|
||||
- 连接池设置
|
||||
- SetMaxIdleConns、SetMaxOpenConns、SetConnMaxLifetime
|
||||
- 连通性测试
|
||||
- Ping 成功后返回 gorm.DB 实例
|
||||
|
||||
章节来源
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
|
||||
### 应用启动中的数据库初始化
|
||||
- 配置加载:Load 会加载 .env 并解析为 Config
|
||||
- 数据库初始化:Init 接收 DatabaseConfig,内部调用 New 完成连接与池设置
|
||||
- 自动迁移:AutoMigrate 按顺序迁移模型,确保外键依赖关系正确
|
||||
- 资源清理:Close 关闭底层 sql.DB
|
||||
|
||||
章节来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L50)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L101-L114)
|
||||
|
||||
## 依赖关系分析
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Config {
|
||||
+ServerConfig Server
|
||||
+DatabaseConfig Database
|
||||
+RedisConfig Redis
|
||||
+RustFSConfig RustFS
|
||||
+JWTConfig JWT
|
||||
+CasbinConfig Casbin
|
||||
+LogConfig Log
|
||||
+UploadConfig Upload
|
||||
+EmailConfig Email
|
||||
}
|
||||
class DatabaseConfig {
|
||||
+string Driver
|
||||
+string Host
|
||||
+int Port
|
||||
+string Username
|
||||
+string Password
|
||||
+string Database
|
||||
+string SSLMode
|
||||
+string Timezone
|
||||
+int MaxIdleConns
|
||||
+int MaxOpenConns
|
||||
+duration ConnMaxLifetime
|
||||
}
|
||||
class Manager {
|
||||
+Init(cfg, logger) error
|
||||
+GetDB() (*gorm.DB, error)
|
||||
+MustGetDB() *gorm.DB
|
||||
+AutoMigrate(logger) error
|
||||
+Close() error
|
||||
}
|
||||
class Postgres {
|
||||
+New(cfg) (*gorm.DB, error)
|
||||
+GetDSN(cfg) string
|
||||
}
|
||||
Config --> DatabaseConfig : "包含"
|
||||
Manager --> DatabaseConfig : "使用"
|
||||
Postgres --> DatabaseConfig : "使用"
|
||||
Manager --> Postgres : "调用"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L13-L24)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L34-L47)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L22-L50)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
|
||||
## 性能考量
|
||||
- 连接池参数建议
|
||||
- max_idle_conns:建议与 max_open_conns 保持一定比例,避免过多空闲连接占用资源
|
||||
- max_open_conns:应结合数据库最大连接数与应用并发量综合评估,避免超过数据库限制导致连接排队
|
||||
- conn_max_lifetime:建议设置为小于数据库连接超时阈值,确保连接健康与复用效率
|
||||
- 日志级别
|
||||
- 当 driver 为 postgres 时,GORM 日志级别提升为 Info,便于调试;在生产环境可考虑降低日志级别以减少开销
|
||||
- 迁移策略
|
||||
- AutoMigrate 顺序需保证被引用表先于引用表创建,避免外键约束冲突
|
||||
|
||||
章节来源
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L25-L32)
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L52-L99)
|
||||
|
||||
## 故障排查指南
|
||||
- 未初始化即使用数据库
|
||||
- 现象:调用 GetDB 返回错误提示“数据库未初始化”
|
||||
- 处理:确保在应用启动阶段先调用 database.Init
|
||||
- 连接失败
|
||||
- 检查 DSN 参数是否正确(host/port/username/password/database/ssl_mode/timezone)
|
||||
- 确认数据库可达与凭据有效
|
||||
- 连接池异常
|
||||
- 检查 max_idle_conns、max_open_conns 是否设置过小或过大
|
||||
- 检查 conn_max_lifetime 是否导致频繁重建连接
|
||||
- 环境变量未生效
|
||||
- 确认环境变量前缀与键名匹配(DATABASE_*)
|
||||
- 确认 overrideFromEnv 可正确解析整型与 duration 类型
|
||||
|
||||
章节来源
|
||||
- [pkg/database/manager.go](file://pkg/database/manager.go#L35-L41)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L13-L60)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
|
||||
|
||||
## 结论
|
||||
- CarrotSkin 的数据库配置采用“默认值 + 环境变量覆盖”的设计,既保证开箱即用,又允许灵活定制
|
||||
- 连接池参数通过 DATABASE_MAX_IDLE_CONNS、DATABASE_MAX_OPEN_CONNS、DATABASE_CONN_MAX_LIFETIME 进行覆盖,建议结合业务并发与数据库能力进行调优
|
||||
- 生产环境建议启用 SSL(ssl_mode)、严格凭据管理与最小权限原则,并适当降低日志级别以优化性能
|
||||
|
||||
## 附录
|
||||
|
||||
### 环境变量与默认值对照表
|
||||
- database.driver
|
||||
- 环境变量:DATABASE_DRIVER
|
||||
- 默认值:postgres
|
||||
- database.host
|
||||
- 环境变量:DATABASE_HOST
|
||||
- 默认值:localhost
|
||||
- database.port
|
||||
- 环境变量:DATABASE_PORT
|
||||
- 默认值:5432
|
||||
- database.username
|
||||
- 环境变量:DATABASE_USERNAME
|
||||
- 默认值:空字符串
|
||||
- database.password
|
||||
- 环境变量:DATABASE_PASSWORD
|
||||
- 默认值:空字符串
|
||||
- database.database
|
||||
- 环境变量:DATABASE_NAME
|
||||
- 默认值:空字符串
|
||||
- database.ssl_mode
|
||||
- 环境变量:DATABASE_SSL_MODE
|
||||
- 默认值:disable
|
||||
- database.timezone
|
||||
- 环境变量:DATABASE_TIMEZONE
|
||||
- 默认值:Asia/Shanghai
|
||||
- database.max_idle_conns
|
||||
- 环境变量:DATABASE_MAX_IDLE_CONNS
|
||||
- 默认值:10
|
||||
- database.max_open_conns
|
||||
- 环境变量:DATABASE_MAX_OPEN_CONNS
|
||||
- 默认值:100
|
||||
- database.conn_max_lifetime
|
||||
- 环境变量:DATABASE_CONN_MAX_LIFETIME
|
||||
- 默认值:1h
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L190-L210)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
|
||||
|
||||
### 生产环境安全与连接池调优建议
|
||||
- 安全建议
|
||||
- 将 ssl_mode 设为 require 或 verify-full(视数据库支持而定),并配合证书校验
|
||||
- 使用专用数据库用户与最小权限策略,定期轮换密码
|
||||
- 通过环境变量注入敏感信息,避免硬编码
|
||||
- 连接池调优
|
||||
- 初步以 max_idle_conns ≈ 1/10 max_open_conns 开始,结合压测结果调整
|
||||
- 将 conn_max_lifetime 设置为略小于数据库连接超时阈值,平衡资源回收与连接复用
|
||||
- 监控数据库连接数与等待队列,动态调整 max_open_conns
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L135-L188)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L238-L280)
|
||||
- [pkg/database/postgres.go](file://pkg/database/postgres.go#L49-L53)
|
||||
|
||||
### 启动脚本中的数据库环境变量示例
|
||||
- 示例脚本展示了如何设置 DATABASE_HOST、DATABASE_PORT、DATABASE_USERNAME、DATABASE_PASSWORD、DATABASE_NAME、DATABASE_SSL_MODE、DATABASE_TIMEZONE 等环境变量
|
||||
|
||||
章节来源
|
||||
- [start.sh](file://start.sh#L1-L41)
|
||||
@@ -1,367 +0,0 @@
|
||||
# 日志配置
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [cmd/server/main.go](file://cmd/server/main.go)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go)
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go)
|
||||
- [go.mod](file://go.mod)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件面向 CarrotSkin 项目的开发者与运维人员,系统性说明日志系统的配置与使用方式。重点围绕以下配置项展开:log.level、format、output、max_size、max_backups、max_age 和 compress;并结合 LogConfig 结构体与实际代码实现,给出不同日志级别选择策略、JSON 格式与文本格式的适用场景、以及基于大小与天数的日志轮转工作机制。最后提供生产环境监控集成建议与最佳实践。
|
||||
|
||||
## 项目结构
|
||||
日志系统由“配置加载层”“日志工厂层”“全局管理器层”“中间件与业务使用层”四部分组成:
|
||||
- 配置加载层:从环境变量加载并解析为 Config,其中包含 LogConfig 字段。
|
||||
- 日志工厂层:根据 LogConfig 构建 zap.Logger。
|
||||
- 全局管理器层:提供 Init、GetLogger、MustGetLogger 等接口,确保单例与线程安全。
|
||||
- 中间件与业务使用层:通过中间件注入日志实例,统一记录 HTTP 请求与异常恢复。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A["配置加载<br/>pkg/config/config.go"] --> B["日志工厂<br/>pkg/logger/logger.go"]
|
||||
B --> C["全局管理器<br/>pkg/logger/manager.go"]
|
||||
C --> D["中间件使用<br/>internal/middleware/logger.go"]
|
||||
C --> E["恢复中间件<br/>internal/middleware/recovery.go"]
|
||||
C --> F["应用入口初始化<br/>cmd/server/main.go"]
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
|
||||
## 核心组件
|
||||
- LogConfig 结构体:定义日志系统的核心配置项,来源于环境变量映射。
|
||||
- New:根据 LogConfig 构建 zap.Logger,支持 JSON/Console 编码器、stdout 或文件输出。
|
||||
- Init/GetLogger/MustGetLogger:全局单例管理,保证线程安全与幂等初始化。
|
||||
- 中间件 Logger/Recovery:在请求处理前后记录日志,并在 panic 时记录堆栈与上下文。
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
|
||||
|
||||
## 架构总览
|
||||
下图展示了从配置到日志输出的关键流程:应用启动时加载配置,初始化日志管理器,随后中间件与业务代码通过全局日志实例进行记录。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as "应用入口<br/>cmd/server/main.go"
|
||||
participant Cfg as "配置加载<br/>pkg/config/config.go"
|
||||
participant LMgr as "日志管理器<br/>pkg/logger/manager.go"
|
||||
participant LNew as "日志工厂<br/>pkg/logger/logger.go"
|
||||
participant MW as "中间件<br/>internal/middleware/logger.go"
|
||||
participant Rec as "恢复中间件<br/>internal/middleware/recovery.go"
|
||||
App->>Cfg : 加载配置
|
||||
Cfg-->>App : 返回 Config(LogConfig)
|
||||
App->>LMgr : Init(LogConfig)
|
||||
LMgr->>LNew : New(LogConfig)
|
||||
LNew-->>LMgr : 返回 zap.Logger
|
||||
LMgr-->>App : 初始化完成
|
||||
App->>MW : 注入日志实例
|
||||
App->>Rec : 注入日志实例
|
||||
MW-->>App : 记录请求日志
|
||||
Rec-->>App : 记录 panic 与堆栈
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L27-L40)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L108-L133)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L20-L29)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L14-L68)
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L10-L39)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L11-L29)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 配置项详解与选择策略
|
||||
- log.level
|
||||
- 取值范围:debug、info、warn、error,默认 info。
|
||||
- 选择策略:
|
||||
- 开发/调试:debug,便于定位问题。
|
||||
- 生产:info,平衡可观测性与性能;仅在需要深入排查时临时提升到 debug。
|
||||
- 严重问题:error,聚焦错误事件,避免过多噪声。
|
||||
- log.format
|
||||
- 取值:console、json。
|
||||
- 适用场景:
|
||||
- console:本地开发终端阅读友好,便于快速定位。
|
||||
- json:便于日志收集与结构化分析,适合生产与集中化监控。
|
||||
- log.output
|
||||
- 取值:空/“stdout”表示输出到标准输出;其他路径将自动创建目录并写入文件。
|
||||
- 适用场景:
|
||||
- 容器化部署:stdout,配合容器日志采集。
|
||||
- 传统部署:文件路径,结合日志轮转策略。
|
||||
- log.max_size、log.max_backups、log.max_age、log.compress
|
||||
- 作用:控制日志轮转策略(按大小与天数)与压缩开关。
|
||||
- 工作机制(基于大小与天数):
|
||||
- 当单个日志文件达到 max_size(MB)时触发轮转。
|
||||
- 最多保留 max_backups 个历史备份文件。
|
||||
- 历史文件保留不超过 max_age(天)。
|
||||
- compress=true 时对旧文件进行压缩,节省磁盘空间。
|
||||
- 注意:当前代码实现中并未直接使用上述字段进行轮转配置,而是采用文件输出与基础写入同步。若需启用基于大小/天数的轮转,可在日志工厂中引入外部轮转库(如 lumberjack)并传入上述参数。
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L170-L178)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L43-L60)
|
||||
|
||||
### 日志级别选择策略
|
||||
- debug:用于开发阶段的详细追踪,包含大量上下文信息。
|
||||
- info:生产环境默认级别,记录关键业务事件与系统状态。
|
||||
- warn:潜在问题或异常流程,需关注但不影响整体运行。
|
||||
- error:错误事件,必须处理与上报。
|
||||
|
||||
章节来源
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L16-L28)
|
||||
|
||||
### JSON 格式与文本格式的适用场景
|
||||
- JSON:
|
||||
- 优点:结构化强,便于日志聚合、检索与告警。
|
||||
- 适用:生产环境、集中化日志平台(如 ELK、Loki、Cloud Logging)。
|
||||
- Console:
|
||||
- 优点:人类可读性强,适合本地开发与快速排障。
|
||||
- 适用:本地调试、临时诊断。
|
||||
|
||||
章节来源
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L30-L41)
|
||||
|
||||
### 日志轮转策略(基于大小与天数)
|
||||
- 当前实现要点:
|
||||
- 输出到文件时会自动创建目录并追加写入。
|
||||
- 未显式配置基于大小/天数的轮转。
|
||||
- 建议扩展方案:
|
||||
- 引入外部轮转库(如 lumberjack),在 New 中根据 max_size、max_backups、max_age、compress 进行轮转配置。
|
||||
- 将轮转配置作为可选参数传入,保持现有接口兼容。
|
||||
|
||||
章节来源
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L43-L60)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
|
||||
|
||||
### 日志工厂与全局管理器
|
||||
- New:根据 LogConfig 构建 zap.Logger,设置级别、编码器与输出目标。
|
||||
- Init/GetLogger/MustGetLogger:提供线程安全的单例初始化与获取能力,避免重复初始化。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class LogConfig {
|
||||
+string level
|
||||
+string format
|
||||
+string output
|
||||
+int max_size
|
||||
+int max_backups
|
||||
+int max_age
|
||||
+bool compress
|
||||
}
|
||||
class LoggerFactory {
|
||||
+New(cfg LogConfig) *zap.Logger
|
||||
}
|
||||
class LogManager {
|
||||
+Init(cfg LogConfig) error
|
||||
+GetLogger() (*zap.Logger, error)
|
||||
+MustGetLogger() *zap.Logger
|
||||
}
|
||||
LogConfig --> LoggerFactory : "输入"
|
||||
LoggerFactory --> LogManager : "被调用"
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L79-L88)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L14-L68)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L20-L46)
|
||||
|
||||
章节来源
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L14-L68)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
|
||||
|
||||
### 中间件与业务使用
|
||||
- Logger 中间件:记录每次 HTTP 请求的方法、路径、状态码、耗时、客户端 IP、User-Agent 等。
|
||||
- Recovery 中间件:捕获 panic 并记录错误、路径、方法、IP 与完整堆栈,返回统一错误响应。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Router as "Gin 路由"
|
||||
participant MW as "日志中间件"
|
||||
participant Handler as "业务处理器"
|
||||
participant Rec as "恢复中间件"
|
||||
Client->>Router : 发起请求
|
||||
Router->>MW : 进入日志中间件
|
||||
MW->>MW : 记录请求开始时间与上下文
|
||||
MW->>Handler : 继续处理
|
||||
Handler-->>MW : 返回响应
|
||||
MW->>MW : 记录状态码、耗时、IP、UA
|
||||
MW-->>Router : 返回响应
|
||||
Note over MW : 若发生 panic,由恢复中间件接管
|
||||
Router->>Rec : 进入恢复中间件
|
||||
Rec->>Rec : 记录错误与堆栈
|
||||
Rec-->>Client : 返回 500 错误
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L10-L39)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L11-L29)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L81-L90)
|
||||
|
||||
章节来源
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L81-L90)
|
||||
|
||||
## 依赖关系分析
|
||||
- 外部依赖:zap 用于高性能日志记录;viper 用于配置加载;godotenv 用于 .env 支持。
|
||||
- 内部依赖:配置模块提供 LogConfig;日志模块提供 Init/GetLogger;中间件依赖日志实例。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "外部依赖"
|
||||
Z["go.uber.org/zap"]
|
||||
V["github.com/spf13/viper"]
|
||||
D["github.com/joho/godotenv"]
|
||||
end
|
||||
subgraph "内部模块"
|
||||
CFG["pkg/config/config.go"]
|
||||
LOGF["pkg/logger/logger.go"]
|
||||
LOGM["pkg/logger/manager.go"]
|
||||
MWL["internal/middleware/logger.go"]
|
||||
MWR["internal/middleware/recovery.go"]
|
||||
MAIN["cmd/server/main.go"]
|
||||
end
|
||||
V --> CFG
|
||||
D --> CFG
|
||||
CFG --> LOGM
|
||||
LOGM --> LOGF
|
||||
MAIN --> LOGM
|
||||
MAIN --> MWL
|
||||
MAIN --> MWR
|
||||
Z --> LOGF
|
||||
Z --> MWL
|
||||
Z --> MWR
|
||||
```
|
||||
|
||||
图表来源
|
||||
- [go.mod](file://go.mod#L7-L22)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
|
||||
章节来源
|
||||
- [go.mod](file://go.mod#L7-L22)
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L1-L69)
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L1-L51)
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L1-L40)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L1-L30)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L1-L124)
|
||||
|
||||
## 性能考量
|
||||
- 使用 zap:具备高性能与零分配特性,适合高并发场景。
|
||||
- 选择合适的日志级别:生产环境默认 info,避免 debug 的额外开销。
|
||||
- 输出目标选择:stdout 便于容器日志采集;文件输出需注意磁盘 IO 与轮转策略。
|
||||
- 压缩与保留策略:合理设置 max_size、max_backups、max_age 与 compress,平衡磁盘占用与检索效率。
|
||||
|
||||
## 故障排查指南
|
||||
- 未初始化日志即使用:
|
||||
- 现象:调用 GetLogger 返回错误提示“日志未初始化,请先调用 logger.Init()”。
|
||||
- 排查:确认应用入口是否在启动时调用了 logger.Init(cfg.Log)。
|
||||
- 输出到文件失败:
|
||||
- 现象:初始化日志时报错,无法打开文件。
|
||||
- 排查:检查 log.output 指定路径是否存在写权限,目录是否可创建。
|
||||
- panic 未记录:
|
||||
- 现象:服务崩溃但无日志记录。
|
||||
- 排查:确认已注册 Recovery 中间件并注入了日志实例。
|
||||
- 日志轮转未生效:
|
||||
- 现象:日志文件持续增大。
|
||||
- 排查:当前实现未使用 max_size/max_backups/max_age/compress 进行轮转,需扩展轮转逻辑。
|
||||
|
||||
章节来源
|
||||
- [pkg/logger/manager.go](file://pkg/logger/manager.go#L31-L37)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L43-L60)
|
||||
- [internal/middleware/recovery.go](file://internal/middleware/recovery.go#L11-L29)
|
||||
- [cmd/server/main.go](file://cmd/server/main.go#L81-L90)
|
||||
|
||||
## 结论
|
||||
- CarrotSkin 的日志系统以 zap 为核心,通过配置驱动实现灵活的日志级别、编码器与输出目标切换。
|
||||
- 当前未直接启用基于大小/天数的轮转,建议在日志工厂中引入轮转库并使用 LogConfig 的 max_size、max_backups、max_age、compress 字段。
|
||||
- 在生产环境中推荐使用 JSON 格式与 stdout 输出,结合集中化日志平台与告警策略,提升可观测性与可维护性。
|
||||
|
||||
## 附录
|
||||
|
||||
### 配置项一览与默认值
|
||||
- log.level:默认 info
|
||||
- log.format:默认 json
|
||||
- log.output:默认 logs/app.log(文件输出)
|
||||
- log.max_size:默认 100 MB
|
||||
- log.max_backups:默认 3
|
||||
- log.max_age:默认 28 天
|
||||
- log.compress:默认 true
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L170-L178)
|
||||
|
||||
### 环境变量映射
|
||||
- LOG_LEVEL → log.level
|
||||
- LOG_FORMAT → log.format
|
||||
- LOG_OUTPUT → log.output
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L224-L228)
|
||||
|
||||
### 配置示例与最佳实践
|
||||
- 示例一:本地开发(控制台输出,便于阅读)
|
||||
- LOG_LEVEL=debug
|
||||
- LOG_FORMAT=console
|
||||
- LOG_OUTPUT=stdout
|
||||
- 示例二:生产(结构化日志,集中化采集)
|
||||
- LOG_LEVEL=info
|
||||
- LOG_FORMAT=json
|
||||
- LOG_OUTPUT=stdout
|
||||
- 示例三:传统服务器(文件输出,配合轮转)
|
||||
- LOG_LEVEL=info
|
||||
- LOG_FORMAT=json
|
||||
- LOG_OUTPUT=/var/log/carrotskin/app.log
|
||||
- max_size=100
|
||||
- max_backups=3
|
||||
- max_age=28
|
||||
- compress=true
|
||||
- 最佳实践:
|
||||
- 生产环境统一使用 JSON 格式与 stdout,便于容器日志采集。
|
||||
- 严格控制日志级别,避免 debug 在生产长期开启。
|
||||
- 对关键路径增加结构化字段(如用户 ID、请求 ID),便于关联追踪。
|
||||
- 在中间件中统一记录请求上下文,确保异常时具备足够信息。
|
||||
- 如需文件轮转,建议在日志工厂中引入轮转库并使用上述配置项。
|
||||
|
||||
章节来源
|
||||
- [pkg/config/config.go](file://pkg/config/config.go#L170-L178)
|
||||
- [pkg/logger/logger.go](file://pkg/logger/logger.go#L30-L41)
|
||||
- [internal/middleware/logger.go](file://internal/middleware/logger.go#L30-L37)
|
||||
@@ -1,205 +0,0 @@
|
||||
# 服务器配置
|
||||
|
||||
<cite>
|
||||
**本文档引用文件**
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [manager.go](file://pkg/config/manager.go)
|
||||
- [main.go](file://cmd/server/main.go)
|
||||
- [start.sh](file://start.sh)
|
||||
- [dev.sh](file://scripts/dev.sh)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [服务器配置项详解](#服务器配置项详解)
|
||||
3. [环境变量覆盖机制](#环境变量覆盖机制)
|
||||
4. [配置示例](#配置示例)
|
||||
5. [性能调优建议](#性能调优建议)
|
||||
6. [配置结构体说明](#配置结构体说明)
|
||||
|
||||
## 简介
|
||||
CarrotSkin项目采用基于环境变量的配置管理机制,通过Viper库实现灵活的配置加载与覆盖。服务器模块的配置主要集中在`ServerConfig`结构体中,定义了端口、运行模式、读写超时等关键参数。本文档详细说明这些配置项的作用、默认值以及如何通过环境变量进行覆盖,为开发者提供清晰的配置指导。
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L26-L32)
|
||||
- [main.go](file://cmd/server/main.go#L27-L124)
|
||||
|
||||
## 服务器配置项详解
|
||||
|
||||
### server.port
|
||||
- **作用**:指定服务器监听的端口号
|
||||
- **数据类型**:string
|
||||
- **默认值**:":8080"
|
||||
- **说明**:该配置值应包含冒号前缀,表示监听所有网络接口上的指定端口。例如":8080"表示监听8080端口。
|
||||
|
||||
### server.mode
|
||||
- **作用**:设置服务器运行模式
|
||||
- **数据类型**:string
|
||||
- **默认值**:"debug"
|
||||
- **说明**:支持"debug"和"production"两种模式。在生产模式下,Gin框架会自动切换到发布模式,关闭调试信息输出,提高性能和安全性。
|
||||
|
||||
### server.read_timeout
|
||||
- **作用**:设置HTTP服务器读取请求的超时时间
|
||||
- **数据类型**:time.Duration
|
||||
- **默认值**:"30s"
|
||||
- **说明**:防止客户端长时间不发送数据导致服务器资源被占用。超时后连接将被关闭。
|
||||
|
||||
### server.write_timeout
|
||||
- **作用**:设置HTTP服务器写入响应的超时时间
|
||||
- **数据类型**:time.Duration
|
||||
- **默认值**:"30s"
|
||||
- **说明**:防止响应过程过长占用服务器资源。对于大文件下载等长时间操作,可能需要适当增加此值。
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L137-L141)
|
||||
- [config.go](file://pkg/config/config.go#L26-L32)
|
||||
|
||||
## 环境变量覆盖机制
|
||||
|
||||
CarrotSkin项目支持通过环境变量覆盖默认配置,实现不同环境下的灵活配置管理。环境变量前缀为`CARROTSKIN`,具体映射关系如下:
|
||||
|
||||
| 配置项 | 环境变量 | 优先级 |
|
||||
|--------|----------|--------|
|
||||
| server.port | CARROTSKIN_SERVER_PORT | 高 |
|
||||
| server.mode | CARROTSKIN_SERVER_MODE | 高 |
|
||||
| server.read_timeout | CARROTSKIN_SERVER_READ_TIMEOUT | 高 |
|
||||
| server.write_timeout | CARROTSKIN_SERVER_WRITE_TIMEOUT | 高 |
|
||||
|
||||
配置加载流程:
|
||||
1. 加载`.env`文件中的环境变量
|
||||
2. 设置各项配置的默认值
|
||||
3. 绑定环境变量映射关系
|
||||
4. 从环境变量中读取并覆盖配置
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[开始] --> B[加载.env文件]
|
||||
B --> C[设置默认配置值]
|
||||
C --> D[绑定环境变量映射]
|
||||
D --> E[从环境变量覆盖配置]
|
||||
E --> F[返回配置实例]
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L108-L134)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L192-L196)
|
||||
- [config.go](file://pkg/config/config.go#L108-L134)
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 开发环境配置
|
||||
在开发环境中,通常使用默认配置或通过`dev.sh`脚本启动:
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
./scripts/dev.sh
|
||||
```
|
||||
|
||||
或者手动设置环境变量:
|
||||
```bash
|
||||
export CARROTSKIN_SERVER_PORT=":8080"
|
||||
export CARROTSKIN_SERVER_MODE="debug"
|
||||
export CARROTSKIN_SERVER_READ_TIMEOUT="30s"
|
||||
export CARROTSKIN_SERVER_WRITE_TIMEOUT="30s"
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
### 生产环境配置
|
||||
生产环境建议使用`start.sh`脚本,并设置更严格的配置:
|
||||
|
||||
```bash
|
||||
# 生产环境启动脚本示例
|
||||
export CARROTSKIN_SERVER_PORT=":80"
|
||||
export CARROTSKIN_SERVER_MODE="production"
|
||||
export CARROTSKIN_SERVER_READ_TIMEOUT="15s"
|
||||
export CARROTSKIN_SERVER_WRITE_TIMEOUT="15s"
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### Docker环境配置
|
||||
在Docker环境中,可以通过环境变量进行配置:
|
||||
|
||||
```dockerfile
|
||||
environment:
|
||||
- CARROTSKIN_SERVER_PORT=:80
|
||||
- CARROTSKIN_SERVER_MODE=production
|
||||
- CARROTSKIN_SERVER_READ_TIMEOUT=20s
|
||||
- CARROTSKIN_SERVER_WRITE_TIMEOUT=20s
|
||||
```
|
||||
|
||||
**Section sources**
|
||||
- [start.sh](file://start.sh#L1-L37)
|
||||
- [dev.sh](file://scripts/dev.sh#L1-L29)
|
||||
|
||||
## 性能调优建议
|
||||
|
||||
### 高并发场景下的超时设置
|
||||
在高并发场景下,合理的超时设置对系统稳定性至关重要:
|
||||
|
||||
- **低延迟场景**:对于API服务等需要快速响应的场景,建议将读写超时设置为10-15秒
|
||||
- **大文件传输场景**:对于包含文件上传下载的功能,建议适当增加write_timeout至60秒以上
|
||||
- **内网服务**:如果是内网服务且网络环境稳定,可以将超时时间设置得更短,如5-10秒
|
||||
|
||||
### 连接池与超时的协同优化
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[客户端请求] --> B{连接建立}
|
||||
B --> |成功| C[读取请求]
|
||||
C --> D{读取超时}
|
||||
D --> |超时| E[关闭连接]
|
||||
D --> |成功| F[处理请求]
|
||||
F --> G[写入响应]
|
||||
G --> H{写入超时}
|
||||
H --> |超时| I[关闭连接]
|
||||
H --> |成功| J[完成响应]
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [main.go](file://cmd/server/main.go#L93-L98)
|
||||
|
||||
**Section sources**
|
||||
- [main.go](file://cmd/server/main.go#L93-L98)
|
||||
- [config.go](file://pkg/config/config.go#L139-L140)
|
||||
|
||||
## 配置结构体说明
|
||||
|
||||
### ServerConfig 结构体
|
||||
```go
|
||||
type ServerConfig struct {
|
||||
Port string `mapstructure:"port"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
ReadTimeout time.Duration `mapstructure:"read_timeout"`
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout"`
|
||||
}
|
||||
```
|
||||
|
||||
- **Port**:端口号,字符串类型,需包含冒号前缀
|
||||
- **Mode**:运行模式,可选值为"debug"或"production"
|
||||
- **ReadTimeout**:读取超时,time.Duration类型,支持"30s"、"1m"等格式
|
||||
- **WriteTimeout**:写入超时,time.Duration类型,支持"30s"、"1m"等格式
|
||||
|
||||
### 配置初始化流程
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Main as main.go
|
||||
participant Config as config.go
|
||||
participant Viper as Viper库
|
||||
Main->>Config : config.Init()
|
||||
Config->>Viper : 加载.env文件
|
||||
Viper-->>Config : 返回环境变量
|
||||
Config->>Config : setDefaults()
|
||||
Config->>Config : setupEnvMappings()
|
||||
Config->>Config : overrideFromEnv()
|
||||
Config-->>Main : 返回配置实例
|
||||
Main->>Main : 启动HTTP服务器
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L108-L134)
|
||||
- [main.go](file://cmd/server/main.go#L27-L32)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L26-L32)
|
||||
- [manager.go](file://pkg/config/manager.go#L19-L28)
|
||||
@@ -1,469 +0,0 @@
|
||||
# 配置管理
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [config.go](file://pkg/config/config.go)
|
||||
- [manager.go](file://pkg/config/manager.go)
|
||||
- [main.go](file://cmd/server/main.go)
|
||||
- [check-env.sh](file://scripts/check-env.sh)
|
||||
- [dev.sh](file://scripts/dev.sh)
|
||||
- [system_config.go](file://internal/model/system_config.go)
|
||||
- [rbac_model.conf](file://configs/casbin/rbac_model.conf)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [配置系统架构](#配置系统架构)
|
||||
3. [配置加载流程](#配置加载流程)
|
||||
4. [可配置选项详解](#可配置选项详解)
|
||||
5. [默认配置与覆盖机制](#默认配置与覆盖机制)
|
||||
6. [配置文件编写指南](#配置文件编写指南)
|
||||
7. [动态配置与环境特定配置](#动态配置与环境特定配置)
|
||||
8. [配置验证与最佳实践](#配置验证与最佳实践)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
|
||||
CarrotSkin项目的配置管理系统采用Viper库实现,提供了一套完整的配置管理解决方案。该系统支持从环境变量、.env文件等多种来源加载配置,确保了应用在不同环境下的灵活性和可移植性。配置系统设计遵循了安全性和易用性的原则,为开发者提供了清晰的配置管理接口。
|
||||
|
||||
本系统的主要特点包括:
|
||||
- 基于Viper库的配置管理
|
||||
- 支持环境变量和.env文件
|
||||
- 线程安全的单例模式
|
||||
- 丰富的默认配置值
|
||||
- 灵活的配置覆盖机制
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [manager.go](file://pkg/config/manager.go#L1-L68)
|
||||
|
||||
## 配置系统架构
|
||||
|
||||
CarrotSkin的配置系统采用分层架构设计,核心组件包括配置结构体、配置管理器和配置加载器。系统通过Viper库实现配置的解析和管理,确保了配置的一致性和可靠性。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[环境变量] --> B[Viper配置管理器]
|
||||
C[.env文件] --> B
|
||||
B --> D[Config结构体]
|
||||
D --> E[ServerConfig]
|
||||
D --> F[DatabaseConfig]
|
||||
D --> G[RedisConfig]
|
||||
D --> H[RustFSConfig]
|
||||
D --> I[JWTConfig]
|
||||
D --> J[LogConfig]
|
||||
D --> K[UploadConfig]
|
||||
D --> L[EmailConfig]
|
||||
M[配置管理器] --> D
|
||||
M --> N[全局配置实例]
|
||||
O[应用程序] --> M
|
||||
style A fill:#f9f,stroke:#333
|
||||
style C fill:#f9f,stroke:#333
|
||||
style B fill:#bbf,stroke:#333
|
||||
style D fill:#ff9,stroke:#333
|
||||
style M fill:#9f9,stroke:#333
|
||||
style O fill:#f96,stroke:#333
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L14-L24)
|
||||
- [manager.go](file://pkg/config/manager.go#L8-L17)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [manager.go](file://pkg/config/manager.go#L1-L68)
|
||||
|
||||
## 配置加载流程
|
||||
|
||||
CarrotSkin的配置加载流程是一个多步骤的过程,确保配置能够正确地从各种来源加载并应用。加载流程包括环境变量前缀设置、默认值配置、环境变量映射和最终的配置解析。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> LoadDotEnv["加载.env文件"]
|
||||
LoadDotEnv --> SetDefaults["设置默认值"]
|
||||
SetDefaults --> SetPrefix["设置环境变量前缀 CARROTSKIN"]
|
||||
SetPrefix --> EnableAutoEnv["启用自动环境变量"]
|
||||
EnableAutoEnv --> SetupMappings["设置环境变量映射"]
|
||||
SetupMappings --> UnmarshalConfig["解析配置到结构体"]
|
||||
UnmarshalConfig --> OverrideEnv["从环境变量覆盖特殊配置"]
|
||||
OverrideEnv --> ReturnConfig["返回配置实例"]
|
||||
ReturnConfig --> End([结束])
|
||||
style Start fill:#4CAF50,stroke:#333
|
||||
style End fill:#4CAF50,stroke:#333
|
||||
style LoadDotEnv fill:#2196F3,stroke:#333
|
||||
style SetDefaults fill:#2196F3,stroke:#333
|
||||
style SetPrefix fill:#2196F3,stroke:#333
|
||||
style EnableAutoEnv fill:#2196F3,stroke:#333
|
||||
style SetupMappings fill:#2196F3,stroke:#333
|
||||
style UnmarshalConfig fill:#2196F3,stroke:#333
|
||||
style OverrideEnv fill:#2196F3,stroke:#333
|
||||
style ReturnConfig fill:#2196F3,stroke:#333
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L108-L132)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L108-L132)
|
||||
|
||||
## 可配置选项详解
|
||||
|
||||
CarrotSkin项目提供了丰富的可配置选项,涵盖了服务器、数据库、Redis、对象存储、JWT、日志、文件上传和邮件等多个模块。每个模块都有详细的配置参数,满足不同场景的需求。
|
||||
|
||||
### 服务器配置
|
||||
|
||||
服务器配置控制应用的基本运行参数,包括端口、模式和超时设置。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| server.port | SERVER_PORT | string | 服务器监听端口 | :8080 |
|
||||
| server.mode | SERVER_MODE | string | 运行模式 (debug/production) | debug |
|
||||
| server.read_timeout | SERVER_READ_TIMEOUT | duration | 读取超时时间 | 30s |
|
||||
| server.write_timeout | SERVER_WRITE_TIMEOUT | duration | 写入超时时间 | 30s |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L26-L32)
|
||||
|
||||
### 数据库配置
|
||||
|
||||
数据库配置管理与PostgreSQL数据库的连接参数和连接池设置。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| database.driver | DATABASE_DRIVER | string | 数据库驱动 | postgres |
|
||||
| database.host | DATABASE_HOST | string | 数据库主机地址 | localhost |
|
||||
| database.port | DATABASE_PORT | int | 数据库端口 | 5432 |
|
||||
| database.username | DATABASE_USERNAME | string | 数据库用户名 | - |
|
||||
| database.password | DATABASE_PASSWORD | string | 数据库密码 | - |
|
||||
| database.database | DATABASE_NAME | string | 数据库名称 | - |
|
||||
| database.ssl_mode | DATABASE_SSL_MODE | string | SSL模式 | disable |
|
||||
| database.timezone | DATABASE_TIMEZONE | string | 时区设置 | Asia/Shanghai |
|
||||
| database.max_idle_conns | DATABASE_MAX_IDLE_CONNS | int | 最大空闲连接数 | 10 |
|
||||
| database.max_open_conns | DATABASE_MAX_OPEN_CONNS | int | 最大打开连接数 | 100 |
|
||||
| database.conn_max_lifetime | DATABASE_CONN_MAX_LIFETIME | duration | 连接最大生命周期 | 1h |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L34-L47)
|
||||
|
||||
### Redis配置
|
||||
|
||||
Redis配置管理Redis缓存服务的连接信息和连接池设置。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| redis.host | REDIS_HOST | string | Redis主机地址 | localhost |
|
||||
| redis.port | REDIS_PORT | int | Redis端口 | 6379 |
|
||||
| redis.password | REDIS_PASSWORD | string | Redis密码 | - |
|
||||
| redis.database | REDIS_DATABASE | int | Redis数据库编号 | 0 |
|
||||
| redis.pool_size | REDIS_POOL_SIZE | int | 连接池大小 | 10 |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L49-L56)
|
||||
|
||||
### 对象存储(RustFS)配置
|
||||
|
||||
对象存储配置管理S3兼容的对象存储服务(RustFS)的连接信息和存储桶设置。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| rustfs.endpoint | RUSTFS_ENDPOINT | string | 对象存储端点 | 127.0.0.1:9000 |
|
||||
| rustfs.access_key | RUSTFS_ACCESS_KEY | string | 访问密钥 | - |
|
||||
| rustfs.secret_key | RUSTFS_SECRET_KEY | string | 密钥 | - |
|
||||
| rustfs.use_ssl | RUSTFS_USE_SSL | bool | 是否使用SSL | false |
|
||||
| rustfs.buckets | RUSTFS_BUCKET_* | map[string]string | 存储桶映射 | - |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L58-L65)
|
||||
|
||||
### JWT配置
|
||||
|
||||
JWT配置管理JSON Web Token的密钥和过期时间设置。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| jwt.secret | JWT_SECRET | string | JWT密钥 | - |
|
||||
| jwt.expire_hours | JWT_EXPIRE_HOURS | int | JWT过期小时数 | 168(7天) |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L67-L71)
|
||||
|
||||
### 日志配置
|
||||
|
||||
日志配置管理应用日志的级别、格式和文件滚动设置。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| log.level | LOG_LEVEL | string | 日志级别 | info |
|
||||
| log.format | LOG_FORMAT | string | 日志格式 | json |
|
||||
| log.output | LOG_OUTPUT | string | 日志输出路径 | logs/app.log |
|
||||
| log.max_size | LOG_MAX_SIZE | int | 单个日志文件最大大小(MB) | 100 |
|
||||
| log.max_backups | LOG_MAX_BACKUPS | int | 保留旧日志文件的最大个数 | 3 |
|
||||
| log.max_age | LOG_MAX_AGE | int | 保留旧日志文件的最大天数 | 28 |
|
||||
| log.compress | LOG_COMPRESS | bool | 是否压缩归档日志 | true |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L79-L88)
|
||||
|
||||
### 文件上传配置
|
||||
|
||||
文件上传配置管理文件上传的大小限制和允许的文件类型。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| upload.max_size | UPLOAD_MAX_SIZE | int64 | 最大上传文件大小 | 10MB |
|
||||
| upload.allowed_types | - | []string | 允许的文件MIME类型 | ["image/png", "image/jpeg"] |
|
||||
| upload.texture_max_size | UPLOAD_TEXTURE_MAX_SIZE | int64 | 纹理文件最大大小 | 2MB |
|
||||
| upload.avatar_max_size | UPLOAD_AVATAR_MAX_SIZE | int64 | 头像文件最大大小 | 1MB |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L90-L96)
|
||||
|
||||
### 邮件配置
|
||||
|
||||
邮件配置管理SMTP邮件服务的连接信息和发件人设置。
|
||||
|
||||
| 配置项 | 环境变量 | 类型 | 描述 | 默认值 |
|
||||
|--------|----------|------|------|--------|
|
||||
| email.enabled | EMAIL_ENABLED | bool | 是否启用邮件服务 | false |
|
||||
| email.smtp_host | EMAIL_SMTP_HOST | string | SMTP主机地址 | - |
|
||||
| email.smtp_port | EMAIL_SMTP_PORT | int | SMTP端口 | 587 |
|
||||
| email.username | EMAIL_USERNAME | string | SMTP用户名 | - |
|
||||
| email.password | EMAIL_PASSWORD | string | SMTP密码 | - |
|
||||
| email.from_name | EMAIL_FROM_NAME | string | 发件人名称 | - |
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L98-L106)
|
||||
|
||||
## 默认配置与覆盖机制
|
||||
|
||||
CarrotSkin的配置系统采用分层覆盖机制,确保配置的灵活性和可靠性。系统首先设置合理的默认值,然后通过环境变量进行覆盖,最后处理特殊的配置覆盖逻辑。
|
||||
|
||||
### 默认配置设置
|
||||
|
||||
系统通过`setDefaults()`函数设置所有配置项的默认值,确保在没有提供外部配置时应用仍能正常运行。
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Config {
|
||||
+Server ServerConfig
|
||||
+Database DatabaseConfig
|
||||
+Redis RedisConfig
|
||||
+RustFS RustFSConfig
|
||||
+JWT JWTConfig
|
||||
+Casbin CasbinConfig
|
||||
+Log LogConfig
|
||||
+Upload UploadConfig
|
||||
+Email EmailConfig
|
||||
}
|
||||
class ServerConfig {
|
||||
+Port string
|
||||
+Mode string
|
||||
+ReadTimeout time.Duration
|
||||
+WriteTimeout time.Duration
|
||||
}
|
||||
class DatabaseConfig {
|
||||
+Driver string
|
||||
+Host string
|
||||
+Port int
|
||||
+Username string
|
||||
+Password string
|
||||
+Database string
|
||||
+SSLMode string
|
||||
+Timezone string
|
||||
+MaxIdleConns int
|
||||
+MaxOpenConns int
|
||||
+ConnMaxLifetime time.Duration
|
||||
}
|
||||
class RedisConfig {
|
||||
+Host string
|
||||
+Port int
|
||||
+Password string
|
||||
+Database int
|
||||
+PoolSize int
|
||||
}
|
||||
class RustFSConfig {
|
||||
+Endpoint string
|
||||
+AccessKey string
|
||||
+SecretKey string
|
||||
+UseSSL bool
|
||||
+Buckets map[string]string
|
||||
}
|
||||
class JWTConfig {
|
||||
+Secret string
|
||||
+ExpireHours int
|
||||
}
|
||||
class LogConfig {
|
||||
+Level string
|
||||
+Format string
|
||||
+Output string
|
||||
+MaxSize int
|
||||
+MaxBackups int
|
||||
+MaxAge int
|
||||
+Compress bool
|
||||
}
|
||||
Config "1" *-- "1" ServerConfig
|
||||
Config "1" *-- "1" DatabaseConfig
|
||||
Config "1" *-- "1" RedisConfig
|
||||
Config "1" *-- "1" RustFSConfig
|
||||
Config "1" *-- "1" JWTConfig
|
||||
Config "1" *-- "1" LogConfig
|
||||
Config "1" *-- "1" UploadConfig
|
||||
Config "1" *-- "1" EmailConfig
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L13-L24)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L135-L188)
|
||||
|
||||
### 配置覆盖流程
|
||||
|
||||
配置覆盖流程确保了配置的优先级顺序:环境变量 > .env文件 > 默认值。系统通过`overrideFromEnv()`函数处理特殊的配置覆盖逻辑。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as 应用程序
|
||||
participant ConfigMgr as 配置管理器
|
||||
participant Viper as Viper库
|
||||
participant Env as 环境变量
|
||||
participant DotEnv as .env文件
|
||||
App->>ConfigMgr : 调用 config.Init()
|
||||
ConfigMgr->>ConfigMgr : 执行 once.Do()
|
||||
ConfigMgr->>DotEnv : 加载 .env 文件
|
||||
ConfigMgr->>Viper : 设置默认值
|
||||
ConfigMgr->>Viper : 设置环境变量前缀 CARROTSKIN
|
||||
ConfigMgr->>Viper : 启用自动环境变量
|
||||
ConfigMgr->>Viper : 绑定环境变量映射
|
||||
Viper->>Env : 读取环境变量
|
||||
Viper->>Viper : 解析配置到结构体
|
||||
ConfigMgr->>Env : 从环境变量覆盖特殊配置
|
||||
ConfigMgr->>App : 返回配置实例
|
||||
```
|
||||
|
||||
**Diagram sources**
|
||||
- [config.go](file://pkg/config/config.go#L108-L132)
|
||||
- [config.go](file://pkg/config/config.go#L238-L304)
|
||||
|
||||
**Section sources**
|
||||
- [config.go](file://pkg/config/config.go#L238-L304)
|
||||
|
||||
## 配置文件编写指南
|
||||
|
||||
为帮助初学者快速上手,以下是配置文件的编写指南。
|
||||
|
||||
### .env文件示例
|
||||
|
||||
创建`.env`文件并填入以下内容:
|
||||
|
||||
```bash
|
||||
# 数据库配置
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_USERNAME=carrot
|
||||
DATABASE_PASSWORD=secret
|
||||
DATABASE_NAME=carrotskin
|
||||
DATABASE_SSL_MODE=disable
|
||||
|
||||
# 对象存储配置
|
||||
RUSTFS_ENDPOINT=127.0.0.1:9000
|
||||
RUSTFS_ACCESS_KEY=minioadmin
|
||||
RUSTFS_SECRET_KEY=minioadmin
|
||||
RUSTFS_USE_SSL=false
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your-jwt-secret-key-change-this-in-production
|
||||
|
||||
# 服务器配置
|
||||
SERVER_PORT=:8080
|
||||
SERVER_MODE=debug
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
|
||||
### 配置验证脚本
|
||||
|
||||
使用提供的`check-env.sh`脚本验证配置的完整性:
|
||||
|
||||
```bash
|
||||
./scripts/check-env.sh
|
||||
```
|
||||
|
||||
该脚本会检查必需的环境变量是否设置,并提供配置概览和安全建议。
|
||||
|
||||
**Section sources**
|
||||
- [check-env.sh](file://scripts/check-env.sh#L1-L78)
|
||||
- [dev.sh](file://scripts/dev.sh#L1-L29)
|
||||
|
||||
## 动态配置与环境特定配置
|
||||
|
||||
对于经验丰富的开发者,CarrotSkin提供了动态配置和环境特定配置的最佳实践。
|
||||
|
||||
### 环境特定配置
|
||||
|
||||
通过环境变量前缀`CARROTSKIN`,可以在不同环境中使用不同的配置:
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
CARROTSKIN_SERVER_PORT=:8080 \
|
||||
CARROTSKIN_DATABASE_HOST=localhost \
|
||||
CARROTSKIN_REDIS_HOST=localhost \
|
||||
go run cmd/server/main.go
|
||||
|
||||
# 生产环境
|
||||
CARROTSKIN_SERVER_PORT=:80 \
|
||||
CARROTSKIN_DATABASE_HOST=prod-db.example.com \
|
||||
CARROTSKIN_REDIS_HOST=prod-redis.example.com \
|
||||
go run cmd/server/main.go
|
||||
```
|
||||
|
||||
### 动态配置管理
|
||||
|
||||
系统提供了线程安全的配置访问接口,支持在运行时安全地获取配置:
|
||||
|
||||
```go
|
||||
// 获取配置实例
|
||||
cfg, err := config.GetConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("配置获取失败: %v", err)
|
||||
}
|
||||
|
||||
// 或使用panic方式获取(确保配置已初始化)
|
||||
cfg := config.MustGetConfig()
|
||||
|
||||
// 获取特定模块配置
|
||||
rustFSConfig := config.MustGetRustFSConfig()
|
||||
```
|
||||
|
||||
**Section sources**
|
||||
- [manager.go](file://pkg/config/manager.go#L19-L63)
|
||||
- [main.go](file://cmd/server/main.go#L27-L124)
|
||||
|
||||
## 配置验证与最佳实践
|
||||
|
||||
### 配置验证
|
||||
|
||||
CarrotSkin提供了多种配置验证机制,确保配置的正确性和安全性:
|
||||
|
||||
1. **必需变量检查**:通过`check-env.sh`脚本检查必需的环境变量
|
||||
2. **配置合理性检查**:检查JWT密钥长度、数据库密码等安全相关配置
|
||||
3. **运行时验证**:在应用启动时验证配置的有效性
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **使用.env文件管理开发配置**:避免将敏感信息硬编码在代码中
|
||||
2. **设置强密码和密钥**:确保JWT密钥至少32字符,使用随机字符串
|
||||
3. **环境隔离**:为不同环境(开发、测试、生产)使用不同的配置
|
||||
4. **配置备份**:定期备份重要的配置文件
|
||||
5. **监控配置变更**:记录配置变更历史,便于问题追踪
|
||||
|
||||
**Section sources**
|
||||
- [check-env.sh](file://scripts/check-env.sh#L1-L78)
|
||||
|
||||
## 结论
|
||||
|
||||
CarrotSkin的配置管理系统提供了一套完整、灵活且安全的配置管理解决方案。通过Viper库的强大功能,系统能够从多种来源加载配置,并提供了丰富的默认值和灵活的覆盖机制。配置系统的设计考虑了不同用户的需求,既为初学者提供了简单的配置文件编写指南,又为经验丰富的开发者提供了动态配置和环境特定配置的最佳实践。
|
||||
|
||||
该系统的线程安全设计和单例模式确保了配置的一致性和可靠性,而详细的配置选项和验证机制则保证了应用的稳定运行。通过遵循本文档提供的指南和最佳实践,开发者可以有效地管理和维护CarrotSkin应用的配置,确保其在不同环境下的正常运行。
|
||||
@@ -1,266 +0,0 @@
|
||||
# 项目概述
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [carrotskin](file://go.mod#L1-L92)
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
- [postgres.go](file://pkg/database/postgres.go#L1-L74)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
- [manager.go](file://pkg/auth/manager.go#L1-L46)
|
||||
- [user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构概述](#架构概述)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖分析](#依赖分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排除指南](#故障排除指南)
|
||||
9. [结论](#结论)
|
||||
|
||||
## 简介
|
||||
CarrotSkin 是一个为 Minecraft 玩家提供皮肤管理服务的后端系统。该项目旨在为用户提供一个稳定、安全且功能丰富的平台,用于上传、管理和分享 Minecraft 皮肤与披风。系统支持用户认证、材质管理、档案系统以及与 Yggdrasil 协议的集成,确保与 Minecraft 客户端的无缝对接。通过 Gin 框架构建的分层架构,CarrotSkin 实现了清晰的职责分离,便于维护和扩展。
|
||||
|
||||
## 项目结构
|
||||
CarrotSkin 项目采用模块化设计,主要分为 `internal` 和 `pkg` 两个目录。`internal` 目录包含应用的核心逻辑,包括处理器(handler)、中间件(middleware)、模型(model)、仓库(repository)和服务(service)。`pkg` 目录则封装了可重用的工具和第三方服务集成,如数据库、Redis、对象存储等。这种结构有助于保持代码的整洁和可维护性。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "内部模块"
|
||||
Handler[handler]
|
||||
Middleware[middleware]
|
||||
Model[model]
|
||||
Repository[repository]
|
||||
Service[service]
|
||||
end
|
||||
subgraph "公共包"
|
||||
Config[config]
|
||||
Database[database]
|
||||
Redis[redis]
|
||||
Storage[storage]
|
||||
Auth[auth]
|
||||
Logger[logger]
|
||||
Utils[utils]
|
||||
end
|
||||
Handler --> Service
|
||||
Service --> Repository
|
||||
Repository --> Database
|
||||
Repository --> Redis
|
||||
Repository --> Storage
|
||||
Auth --> Middleware
|
||||
```
|
||||
|
||||
**图源**
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
|
||||
**本节来源**
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
|
||||
## 核心组件
|
||||
CarrotSkin 的核心组件包括用户认证、材质管理、档案系统和 Yggdrasil 协议集成。用户认证模块负责用户的注册、登录和权限验证,使用 JWT 进行安全的会话管理。材质管理模块允许用户上传、搜索和下载皮肤与披风,支持收藏和下载统计。档案系统为每个用户提供了多个 Minecraft 角色档案,支持设置活跃档案和管理皮肤与披风。Yggdrasil 协议集成确保了与 Minecraft 客户端的兼容性,支持身份验证和服务器加入。
|
||||
|
||||
**本节来源**
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
|
||||
## 架构概述
|
||||
CarrotSkin 采用基于 Gin 框架的分层架构,分为处理器(Handler)、服务(Service)和仓库(Repository)三层。处理器层负责处理 HTTP 请求和响应,服务层实现业务逻辑,仓库层负责数据访问。这种分层设计确保了各层之间的职责分离,提高了代码的可测试性和可维护性。系统与 PostgreSQL、Redis 和 MinIO/RustFS 等外部系统集成,分别用于持久化存储、缓存和对象存储。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Client[客户端] --> Handler[处理器]
|
||||
Handler --> Service[服务]
|
||||
Service --> Repository[仓库]
|
||||
Repository --> PostgreSQL[(PostgreSQL)]
|
||||
Repository --> Redis[(Redis)]
|
||||
Repository --> MinIO[(MinIO/RustFS)]
|
||||
```
|
||||
|
||||
**图源**
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
|
||||
**本节来源**
|
||||
- [routes.go](file://internal/handler/routes.go#L1-L140)
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
|
||||
## 详细组件分析
|
||||
### 用户认证分析
|
||||
用户认证模块是 CarrotSkin 的安全基石,负责用户的注册、登录和权限验证。通过 JWT 实现无状态的会话管理,确保了系统的可扩展性。用户信息存储在 PostgreSQL 中,密码经过哈希处理以保证安全性。Redis 用于缓存用户会话和验证码,提高响应速度。
|
||||
|
||||
#### 类图
|
||||
```mermaid
|
||||
classDiagram
|
||||
class User {
|
||||
+int64 ID
|
||||
+string Username
|
||||
+string Email
|
||||
+string Avatar
|
||||
+int Points
|
||||
+string Role
|
||||
+int16 Status
|
||||
+string Properties
|
||||
+*time.Time LastLoginAt
|
||||
+time.Time CreatedAt
|
||||
+time.Time UpdatedAt
|
||||
}
|
||||
class Token {
|
||||
+int64 ID
|
||||
+int64 UserID
|
||||
+string AccessToken
|
||||
+string RefreshToken
|
||||
+string ProfileId
|
||||
+time.Time ExpiresAt
|
||||
+time.Time CreatedAt
|
||||
}
|
||||
class Yggdrasil {
|
||||
+int64 ID
|
||||
+string Password
|
||||
}
|
||||
User --> Token : "拥有"
|
||||
User --> Yggdrasil : "关联"
|
||||
```
|
||||
|
||||
**图源**
|
||||
- [user.go](file://internal/model/user.go#L1-L71)
|
||||
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
|
||||
**本节来源**
|
||||
- [user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [auth.go](file://internal/middleware/auth.go#L1-L79)
|
||||
|
||||
### 材质管理分析
|
||||
材质管理模块允许用户上传、搜索和下载 Minecraft 皮肤与披风。每个材质记录包含上传者信息、名称、描述、类型、URL、哈希值等元数据。系统通过 MinIO/RustFS 存储实际的材质文件,PostgreSQL 存储元数据。Redis 用于缓存热门材质和下载统计,提高性能。
|
||||
|
||||
#### 序列图
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "客户端"
|
||||
participant Handler as "处理器"
|
||||
participant Service as "服务"
|
||||
participant Repository as "仓库"
|
||||
participant MinIO as "MinIO/RustFS"
|
||||
Client->>Handler : 上传材质
|
||||
Handler->>Service : 调用CreateTexture
|
||||
Service->>Repository : 检查哈希是否存在
|
||||
Repository->>PostgreSQL : 查询数据库
|
||||
Repository-->>Service : 返回结果
|
||||
Service->>MinIO : 生成预签名URL
|
||||
MinIO-->>Service : 返回URL
|
||||
Service-->>Handler : 返回上传URL
|
||||
Handler-->>Client : 返回预签名URL
|
||||
```
|
||||
|
||||
**图源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [minio.go](file://pkg/storage/minio.go#L1-L121)
|
||||
|
||||
**本节来源**
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [texture.go](file://internal/model/texture.go#L1-L77)
|
||||
|
||||
### 档案系统分析
|
||||
档案系统为每个用户提供了多个 Minecraft 角色档案,支持设置活跃档案和管理皮肤与披风。每个档案包含 UUID、用户名、皮肤和披风的引用。系统通过 RSA 密钥对确保档案的安全性,支持 Yggdrasil 协议的身份验证。
|
||||
|
||||
#### 流程图
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([创建档案]) --> ValidateUser["验证用户存在"]
|
||||
ValidateUser --> CheckName["检查角色名是否已存在"]
|
||||
CheckName --> GenerateUUID["生成UUID"]
|
||||
GenerateUUID --> GenerateKey["生成RSA密钥对"]
|
||||
GenerateKey --> CreateProfile["创建档案记录"]
|
||||
CreateProfile --> SetActive["设置为活跃档案"]
|
||||
SetActive --> End([档案创建成功])
|
||||
```
|
||||
|
||||
**图源**
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
**本节来源**
|
||||
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
|
||||
- [profile.go](file://internal/model/profile.go#L1-L64)
|
||||
|
||||
### Yggdrasil 协议集成分析
|
||||
Yggdrasil 协议集成确保了 CarrotSkin 与 Minecraft 客户端的兼容性。系统实现了身份验证、令牌验证、刷新和服务器加入等功能。通过 Redis 存储会话信息,确保玩家在加入服务器时的身份验证。
|
||||
|
||||
#### 序列图
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as "Minecraft客户端"
|
||||
participant Handler as "处理器"
|
||||
participant Service as "服务"
|
||||
participant Repository as "仓库"
|
||||
participant Redis as "Redis"
|
||||
Client->>Handler : authenticate
|
||||
Handler->>Service : 调用Authenticate
|
||||
Service->>Repository : 验证用户凭证
|
||||
Repository->>PostgreSQL : 查询用户
|
||||
Repository-->>Service : 返回用户信息
|
||||
Service->>Redis : 存储会话数据
|
||||
Redis-->>Service : 确认存储
|
||||
Service-->>Handler : 返回认证结果
|
||||
Handler-->>Client : 返回访问令牌
|
||||
```
|
||||
|
||||
**图源**
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
- [redis.go](file://pkg/redis/redis.go#L1-L175)
|
||||
|
||||
**本节来源**
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
- [yggdrasil.go](file://internal/model/yggdrasil.go#L1-L49)
|
||||
|
||||
## 依赖分析
|
||||
CarrotSkin 依赖于多个外部系统和库,包括 PostgreSQL 用于持久化存储,Redis 用于缓存和会话管理,MinIO/RustFS 用于对象存储。Gin 框架提供了高效的 HTTP 路由和中间件支持,GORM 简化了数据库操作。JWT 用于安全的用户认证,Viper 用于配置管理。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
CarrotSkin --> PostgreSQL
|
||||
CarrotSkin --> Redis
|
||||
CarrotSkin --> MinIO
|
||||
CarrotSkin --> Gin
|
||||
CarrotSkin --> GORM
|
||||
CarrotSkin --> JWT
|
||||
CarrotSkin --> Viper
|
||||
```
|
||||
|
||||
**图源**
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
|
||||
**本节来源**
|
||||
- [go.mod](file://go.mod#L1-L92)
|
||||
- [config.go](file://pkg/config/config.go#L1-L305)
|
||||
|
||||
## 性能考虑
|
||||
CarrotSkin 在设计时充分考虑了性能优化。通过 Redis 缓存频繁访问的数据,如用户会话、验证码和热门材质,减少了数据库的负载。MinIO/RustFS 提供了高效的对象存储,支持大规模文件上传和下载。Gin 框架的高性能特性确保了快速的请求处理。此外,系统通过分页和限制查询结果数量,避免了大数据量查询对性能的影响。
|
||||
|
||||
## 故障排除指南
|
||||
在使用 CarrotSkin 时,可能会遇到一些常见问题。例如,用户无法登录可能是由于密码错误或账户被禁用。材质上传失败可能是由于文件大小超出限制或存储桶配置错误。Yggdrasil 身份验证失败可能是由于令牌过期或会话数据不匹配。建议检查日志文件以获取详细的错误信息,并确保所有外部服务(如 PostgreSQL、Redis 和 MinIO)正常运行。
|
||||
|
||||
**本节来源**
|
||||
- [user_service.go](file://internal/service/user_service.go#L1-L249)
|
||||
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
|
||||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L1-L202)
|
||||
|
||||
## 结论
|
||||
CarrotSkin 是一个功能丰富且安全可靠的 Minecraft 皮肤站后端服务。通过分层架构和模块化设计,系统实现了清晰的职责分离,便于维护和扩展。与 PostgreSQL、Redis 和 MinIO/RustFS 的集成确保了数据的持久性和高性能。Yggdrasil 协议的支持使得系统能够无缝对接 Minecraft 客户端。未来,可以进一步优化性能,增加更多的用户功能,如皮肤编辑器和社区互动。
|
||||
File diff suppressed because one or more lines are too long
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装必要的运行时依赖
|
||||
RUN apk add --no-cache ca-certificates tzdata wget
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -g 1000 appuser && \
|
||||
adduser -D -u 1000 -G appuser appuser
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制已经编译好的二进制文件
|
||||
ARG BINARY_NAME=mcauth-linux-amd64
|
||||
COPY ${BINARY_NAME} /app/server
|
||||
|
||||
# 复制配置文件(如果需要)
|
||||
COPY configs/ /app/configs/
|
||||
|
||||
# 设置权限
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# 切换到非 root 用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["/app/server"]
|
||||
646
README.md
646
README.md
@@ -1,564 +1,160 @@
|
||||
# CarrotSkin Backend
|
||||
|
||||
一个功能完善的Minecraft皮肤站后端系统,采用单体架构设计,基于Go语言和Gin框架开发。
|
||||
一个功能完善的 Minecraft 皮肤站后端,基于 Go + Gin 构建,覆盖用户认证、材质管理、角色档案、审计日志等核心能力,并提供完整的 Swagger 文档与容器友好的环境变量配置。
|
||||
|
||||
## ✨ 核心功能
|
||||
## ✨ 功能亮点
|
||||
|
||||
- ✅ **用户认证系统** - 注册、登录、JWT认证、积分系统
|
||||
- ✅ **邮箱验证系统** - 注册验证、找回密码、更换邮箱(基于Redis的验证码)
|
||||
- ✅ **材质管理系统** - 皮肤/披风上传、搜索、收藏、下载统计
|
||||
- ✅ **角色档案系统** - Minecraft角色创建、管理、RSA密钥生成
|
||||
- ✅ **文件存储** - MinIO/RustFS对象存储集成、预签名URL上传
|
||||
- ✅ **缓存系统** - Redis缓存、验证码存储、频率限制
|
||||
- ✅ **权限管理** - Casbin RBAC权限控制
|
||||
- ✅ **数据审计** - 登录日志、操作审计、下载记录
|
||||
- **账号体系**:注册 / 登录 / JWT 鉴权 / Yggdrasil 密码同步 / 用户积分
|
||||
- **邮箱与验证码**:验证码发送频率控制、邮箱绑定与变更
|
||||
- **材质中心**:皮肤/披风上传、搜索、收藏、下载统计、Hash 去重
|
||||
- **角色档案**:Minecraft Profile 管理、RSA 密钥对生成、活跃档案切换
|
||||
- **存储与上传**:RustFS/MinIO 预签名 URL,减轻服务器带宽压力
|
||||
- **任务与日志**:登录日志、操作审计、材质下载记录、定时任务
|
||||
- **权限体系**:Casbin RBAC,支持细粒度路线授权
|
||||
- **配置管理**:100% 依赖环境变量,`SERVER_SWAGGER_ENABLED` 控制 Swagger
|
||||
- **可观测性**:Zap 结构化日志、统一 API 响应模型
|
||||
|
||||
## 项目结构
|
||||
## 🛠 技术栈
|
||||
|
||||
| 类型 | 选型 |
|
||||
| --- | --- |
|
||||
| 语言 / 运行时 | Go 1.24+ |
|
||||
| Web 框架 | Gin |
|
||||
| ORM | GORM (PostgreSQL 驱动) |
|
||||
| 数据库 | PostgreSQL 15+ |
|
||||
| 缓存 / 消息 | Redis 6+ |
|
||||
| 对象存储 | RustFS / MinIO(S3 兼容) |
|
||||
| 权限控制 | Casbin |
|
||||
| 配置 | Viper + `.env` |
|
||||
| API 文档 | swaggo / Swagger UI |
|
||||
| 日志 | Uber Zap |
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/ # 应用程序入口
|
||||
│ └── server/ # 主服务器入口
|
||||
│ └── main.go # 服务初始化、路由注册
|
||||
├── internal/ # 私有应用代码
|
||||
│ ├── handler/ # HTTP处理器(函数式)
|
||||
│ │ ├── routes.go # 路由注册
|
||||
│ │ ├── auth_handler.go
|
||||
│ │ ├── user_handler.go
|
||||
│ │ └── ...
|
||||
│ ├── service/ # 业务逻辑服务(函数式)
|
||||
│ │ ├── common.go # 公共声明(jsoniter等)
|
||||
│ │ ├── user_service.go
|
||||
│ │ └── ...
|
||||
│ ├── repository/ # 数据访问层(函数式)
|
||||
│ │ ├── user_repository.go
|
||||
│ │ └── ...
|
||||
│ ├── model/ # 数据模型(GORM)
|
||||
│ ├── middleware/ # 中间件
|
||||
│ └── types/ # 类型定义
|
||||
├── pkg/ # 公共库代码
|
||||
│ ├── auth/ # 认证授权
|
||||
│ │ └── manager.go # JWT服务管理器
|
||||
│ ├── config/ # 配置管理
|
||||
│ │ └── manager.go # 配置管理器
|
||||
│ ├── database/ # 数据库连接
|
||||
│ │ ├── manager.go # 数据库管理器(AutoMigrate)
|
||||
│ │ └── postgres.go # PostgreSQL连接
|
||||
│ ├── email/ # 邮件服务
|
||||
│ │ └── manager.go # 邮件服务管理器
|
||||
│ ├── logger/ # 日志系统
|
||||
│ │ └── manager.go # 日志管理器
|
||||
│ ├── redis/ # Redis客户端
|
||||
│ │ └── manager.go # Redis管理器
|
||||
│ ├── storage/ # 文件存储(RustFS/MinIO)
|
||||
│ │ └── manager.go # 存储管理器
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── validator/ # 数据验证
|
||||
├── docs/ # API定义和文档(Swagger)
|
||||
├── configs/ # 配置文件
|
||||
│ └── casbin/ # Casbin权限配置
|
||||
├── go.mod # Go模块依赖
|
||||
├── go.sum # Go模块校验
|
||||
├── start.sh # Linux/Mac启动脚本
|
||||
├── .env # 环境变量配置
|
||||
└── README.md # 项目说明
|
||||
├── cmd/server/ # 应用入口(main.go)
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP Handler 与 Swagger 注解
|
||||
│ ├── service/ # 业务逻辑
|
||||
│ ├── repository/ # 数据访问
|
||||
│ ├── model/ # GORM 数据模型
|
||||
│ ├── types/ # 请求/响应 DTO
|
||||
│ ├── middleware/ # Gin 中间件
|
||||
│ └── task/ # 定时任务与后台作业
|
||||
├── pkg/ # 可复用组件(config、database、auth、logger、redis、storage 等)
|
||||
├── docs/ # swagger 生成产物(docs.go / swagger.json / swagger.yaml)
|
||||
├── start.sh # 启动脚本(自动 swag init)
|
||||
├── docker-compose.yml # 本地容器编排
|
||||
├── .env.example # 环境变量示例
|
||||
└── go.mod # Go Module 定义
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
## ✅ 前置要求
|
||||
|
||||
- **语言**: Go 1.23+
|
||||
- **框架**: Gin Web Framework
|
||||
- **数据库**: PostgreSQL 15+ (GORM ORM)
|
||||
- **缓存**: Redis 6.0+
|
||||
- **存储**: RustFS/MinIO (S3兼容对象存储)
|
||||
- **权限**: Casbin RBAC
|
||||
- **日志**: Zap (结构化日志)
|
||||
- **配置**: 环境变量 (.env) + Viper
|
||||
- **JSON**: jsoniter (高性能JSON序列化)
|
||||
- **文档**: Swagger/OpenAPI 3.0
|
||||
- Go 1.24+
|
||||
- PostgreSQL 15+
|
||||
- Redis 6+
|
||||
- RustFS / MinIO(或其他兼容 S3 的对象存储,用于皮肤与头像)
|
||||
|
||||
## 快速开始
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Go 1.21或更高版本
|
||||
- PostgreSQL 15或更高版本
|
||||
- Redis 6.0或更高版本
|
||||
- RustFS 或其他 S3 兼容对象存储服务
|
||||
|
||||
### 安装和运行
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd CarrotSkin/backend
|
||||
```
|
||||
1. **克隆仓库**
|
||||
```bash
|
||||
git clone <repo>
|
||||
cd backend
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
3. **配置环境**
|
||||
```bash
|
||||
# 复制环境变量文件
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件配置数据库、RustFS等服务连接信息
|
||||
```
|
||||
|
||||
**注意**:项目完全依赖 `.env` 文件进行配置,不再使用 YAML 配置文件,便于 Docker 容器化部署。
|
||||
3. **配置环境变量**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 根据实际环境填写数据库、Redis、对象存储、邮件等信息
|
||||
```
|
||||
|
||||
4. **初始化数据库**
|
||||
```bash
|
||||
# 创建数据库
|
||||
createdb carrotskin
|
||||
# 或者使用PostgreSQL客户端
|
||||
psql -h localhost -U postgres -c "CREATE DATABASE carrotskin;"
|
||||
```
|
||||
```bash
|
||||
createdb carrotskin
|
||||
# 或 psql -c "CREATE DATABASE carrotskin;"
|
||||
```
|
||||
> 应用启动时会执行 `AutoMigrate`,自动创建 / 更新表结构。
|
||||
|
||||
> 💡 **提示**: 项目使用 GORM 的 `AutoMigrate` 功能自动创建和更新数据库表结构,无需手动执行SQL脚本。首次启动时会自动创建所有表。
|
||||
5. **启动服务**
|
||||
- **推荐**:`./start.sh`(自动 `swag init`,随后 `go run cmd/server/main.go`)
|
||||
- **手动启动**:
|
||||
```bash
|
||||
swag init -g cmd/server/main.go -o docs
|
||||
go run cmd/server/main.go
|
||||
```
|
||||
|
||||
5. **运行服务**
|
||||
6. **访问接口**
|
||||
- API Root: `http://localhost:8080`
|
||||
- Swagger: `http://localhost:8080/swagger/index.html`(需 `SERVER_SWAGGER_ENABLED=true`)
|
||||
|
||||
方式一:使用启动脚本(推荐)
|
||||
```bash
|
||||
# Linux/Mac
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
## ⚙️ 关键环境变量
|
||||
|
||||
# Windows
|
||||
start.bat
|
||||
```
|
||||
| 变量 | 说明 | 示例 |
|
||||
| --- | --- | --- |
|
||||
| `SERVER_PORT` | 服务监听端口 | `8080` |
|
||||
| `SERVER_MODE` | Gin 模式(debug/release) | `debug` |
|
||||
| `SERVER_SWAGGER_ENABLED` | 是否暴露 Swagger UI | `true` |
|
||||
| `DATABASE_HOST` / `DATABASE_PORT` | PostgreSQL 地址 | `localhost` / `5432` |
|
||||
| `DATABASE_USERNAME` / `DATABASE_PASSWORD` | 数据库凭据 | `postgres` |
|
||||
| `DATABASE_NAME` | 数据库名称 | `carrotskin` |
|
||||
| `REDIS_HOST` / `REDIS_PORT` | Redis 地址 | `localhost` / `6379` |
|
||||
| `REDIS_PASSWORD` | Redis 密码(无可为空) | `` |
|
||||
| `RUSTFS_ENDPOINT` | RustFS/MinIO 访问地址 | `127.0.0.1:9000` |
|
||||
| `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` | 对象存储凭据 | `minioadmin` |
|
||||
| `RUSTFS_BUCKET_TEXTURES` / `RUSTFS_BUCKET_AVATARS` | 存储桶名称 | `carrotskin-textures` |
|
||||
| `JWT_SECRET` | JWT 签名密钥 | `change-me` |
|
||||
| `EMAIL_ENABLED` | 是否开启邮件服务 | `true` |
|
||||
| `EMAIL_SMTP_HOST` / `EMAIL_SMTP_PORT` | SMTP 配置 | `smtp.example.com` / `587` |
|
||||
|
||||
方式二:直接运行
|
||||
```bash
|
||||
# 设置环境变量(或使用.env文件)
|
||||
export DATABASE_HOST=localhost
|
||||
export DATABASE_PORT=5432
|
||||
# ... 其他环境变量
|
||||
更多变量请参考 `.env.example` 与 `.env.docker.example`。
|
||||
|
||||
# 运行服务
|
||||
go run cmd/server/main.go
|
||||
```
|
||||
|
||||
> 💡 **提示**:
|
||||
> - 启动脚本会自动加载 `.env` 文件中的环境变量
|
||||
> - 首次启动时会自动执行数据库迁移(AutoMigrate)
|
||||
> - 如果对象存储未配置,服务仍可启动(相关功能不可用)
|
||||
|
||||
服务启动后:
|
||||
- **服务地址**: http://localhost:8080
|
||||
- **Swagger文档**: http://localhost:8080/swagger/index.html
|
||||
- **健康检查**: http://localhost:8080/health
|
||||
|
||||
## API接口
|
||||
|
||||
### 认证相关
|
||||
- `POST /api/v1/auth/register` - 用户注册(需邮箱验证码)
|
||||
- `POST /api/v1/auth/login` - 用户登录(支持用户名/邮箱)
|
||||
- `POST /api/v1/auth/send-code` - 发送验证码(注册/重置密码/更换邮箱)
|
||||
- `POST /api/v1/auth/reset-password` - 重置密码(需验证码)
|
||||
|
||||
### 用户相关(需认证)
|
||||
- `GET /api/v1/user/profile` - 获取用户信息
|
||||
- `PUT /api/v1/user/profile` - 更新用户信息(头像、密码)
|
||||
- `POST /api/v1/user/avatar/upload-url` - 生成头像上传URL
|
||||
- `PUT /api/v1/user/avatar` - 更新头像
|
||||
- `POST /api/v1/user/change-email` - 更换邮箱(需验证码)
|
||||
|
||||
### 材质管理
|
||||
公开接口:
|
||||
- `GET /api/v1/texture` - 搜索材质
|
||||
- `GET /api/v1/texture/:id` - 获取材质详情
|
||||
|
||||
认证接口:
|
||||
- `POST /api/v1/texture/upload-url` - 生成材质上传URL
|
||||
- `POST /api/v1/texture` - 创建材质记录
|
||||
- `PUT /api/v1/texture/:id` - 更新材质
|
||||
- `DELETE /api/v1/texture/:id` - 删除材质
|
||||
- `POST /api/v1/texture/:id/favorite` - 切换收藏状态
|
||||
- `GET /api/v1/texture/my` - 我的材质列表
|
||||
- `GET /api/v1/texture/favorites` - 我的收藏列表
|
||||
|
||||
### 角色档案
|
||||
公开接口:
|
||||
- `GET /api/v1/profile/:uuid` - 获取档案详情
|
||||
|
||||
认证接口:
|
||||
- `POST /api/v1/profile` - 创建角色档案(UUID由后端生成)
|
||||
- `GET /api/v1/profile` - 我的档案列表
|
||||
- `PUT /api/v1/profile/:uuid` - 更新档案
|
||||
- `DELETE /api/v1/profile/:uuid` - 删除档案
|
||||
- `POST /api/v1/profile/:uuid/activate` - 设置活跃档案
|
||||
|
||||
### 系统配置
|
||||
- `GET /api/v1/system/config` - 获取系统配置
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
项目**完全依赖环境变量**进行配置,不使用 YAML 配置文件,便于容器化部署:
|
||||
|
||||
1. **配置来源**: 环境变量 或 `.env` 文件
|
||||
2. **环境变量格式**: 使用下划线分隔,全大写,如 `DATABASE_HOST`
|
||||
3. **容器部署**: 直接在容器运行时设置环境变量即可
|
||||
|
||||
**主要环境变量**:
|
||||
```bash
|
||||
# 数据库配置
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_USERNAME=postgres
|
||||
DATABASE_PASSWORD=your_password
|
||||
DATABASE_NAME=carrotskin
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
REDIS_DATABASE=0
|
||||
REDIS_POOL_SIZE=10
|
||||
|
||||
# RustFS对象存储配置 (S3兼容)
|
||||
RUSTFS_ENDPOINT=127.0.0.1:9000
|
||||
RUSTFS_ACCESS_KEY=your_access_key
|
||||
RUSTFS_SECRET_KEY=your_secret_key
|
||||
RUSTFS_USE_SSL=false
|
||||
RUSTFS_BUCKET_TEXTURES=carrot-skin-textures
|
||||
RUSTFS_BUCKET_AVATARS=carrot-skin-avatars
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your-jwt-secret-key
|
||||
JWT_EXPIRE_HOURS=168
|
||||
|
||||
# 邮件配置
|
||||
EMAIL_ENABLED=true
|
||||
EMAIL_SMTP_HOST=smtp.example.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_USERNAME=noreply@example.com
|
||||
EMAIL_PASSWORD=your_email_password
|
||||
EMAIL_FROM_NAME=CarrotSkin
|
||||
```
|
||||
|
||||
**动态配置(存储在数据库中)**:
|
||||
- 积分系统配置(注册奖励、签到积分等)
|
||||
- 用户限制配置(最大材质数、最大角色数等)
|
||||
- 网站设置(站点名称、公告、维护模式等)
|
||||
|
||||
完整的环境变量列表请参考 `.env.example` 文件。
|
||||
|
||||
### 数据库自动迁移
|
||||
|
||||
项目使用 GORM 的 `AutoMigrate` 功能自动管理数据库表结构:
|
||||
|
||||
- **首次启动**: 自动创建所有表结构
|
||||
- **模型更新**: 自动添加新字段、索引等
|
||||
- **类型转换**: 自动处理字段类型变更(如枚举类型转为varchar)
|
||||
- **外键管理**: 自动管理外键关系
|
||||
|
||||
**注意事项**:
|
||||
- 生产环境建议先备份数据库再执行迁移
|
||||
- 某些复杂变更(如删除字段)可能需要手动处理
|
||||
- 枚举类型在PostgreSQL中存储为varchar,避免类型兼容问题
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 面向过程的函数式架构
|
||||
|
||||
项目采用**面向过程的函数式架构**,摒弃不必要的面向对象抽象,使用独立函数和单例管理器模式,代码更简洁、可维护性更强:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Handler 层 (函数) │ ← 路由处理、参数验证、响应格式化
|
||||
├─────────────────────────────────────┤
|
||||
│ Service 层 (函数) │ ← 业务逻辑、权限检查、数据验证
|
||||
├─────────────────────────────────────┤
|
||||
│ Repository 层 (函数) │ ← 数据库操作、关联查询
|
||||
├─────────────────────────────────────┤
|
||||
│ Manager 层 (单例模式) │ ← 核心依赖管理(线程安全)
|
||||
│ - database.MustGetDB() │
|
||||
│ - logger.MustGetLogger() │
|
||||
│ - auth.MustGetJWTService() │
|
||||
│ - redis.MustGetClient() │
|
||||
│ - email.MustGetService() │
|
||||
│ - storage.MustGetClient() │
|
||||
│ - config.MustGetConfig() │
|
||||
├──────────────┬──────────────────────┤
|
||||
│ PostgreSQL │ Redis │ RustFS │ ← 数据存储层
|
||||
└──────────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
### 架构特点
|
||||
|
||||
1. **函数式设计**: 所有业务逻辑以独立函数形式实现,无结构体方法,降低耦合度
|
||||
2. **管理器模式**: 使用 `sync.Once` 实现线程安全的单例管理器,统一管理核心依赖
|
||||
3. **按需获取**: 通过管理器函数按需获取依赖,避免链式传递,代码更清晰
|
||||
4. **自动迁移**: 使用 GORM AutoMigrate 自动管理数据库表结构
|
||||
5. **高性能**: 使用 jsoniter 替代标准库 json,提升序列化性能
|
||||
|
||||
### 核心模块
|
||||
|
||||
1. **认证模块** (`internal/handler/auth_handler.go`)
|
||||
- JWT令牌生成和验证(通过 `auth.MustGetJWTService()` 获取)
|
||||
- bcrypt密码加密
|
||||
- 邮箱验证码注册
|
||||
- 密码重置功能
|
||||
- 登录日志记录(支持用户名/邮箱登录)
|
||||
|
||||
2. **用户模块** (`internal/handler/user_handler.go`)
|
||||
- 用户信息管理
|
||||
- 头像上传(预签名URL,通过 `storage.MustGetClient()` 获取)
|
||||
- 密码修改(需原密码验证)
|
||||
- 邮箱更换(需验证码)
|
||||
- 积分系统
|
||||
|
||||
3. **邮箱验证模块** (`internal/service/verification_service.go`)
|
||||
- 验证码生成(6位数字)
|
||||
- 验证码存储(Redis,10分钟有效期,通过 `redis.MustGetClient()` 获取)
|
||||
- 发送频率限制(1分钟)
|
||||
- 邮件发送(HTML格式,通过 `email.MustGetService()` 获取)
|
||||
|
||||
4. **材质模块** (`internal/handler/texture_handler.go`)
|
||||
- 材质上传(预签名URL)
|
||||
- 材质搜索和收藏
|
||||
- Hash去重
|
||||
- 下载统计
|
||||
|
||||
5. **档案模块** (`internal/handler/profile_handler.go`)
|
||||
- Minecraft角色管理
|
||||
- RSA密钥生成(RSA-2048)
|
||||
- 活跃状态管理
|
||||
- 档案数量限制
|
||||
|
||||
6. **管理器模块** (`pkg/*/manager.go`)
|
||||
- 数据库管理器:`database.MustGetDB()` - 线程安全的数据库连接
|
||||
- 日志管理器:`logger.MustGetLogger()` - 结构化日志实例
|
||||
- JWT管理器:`auth.MustGetJWTService()` - JWT服务实例
|
||||
- Redis管理器:`redis.MustGetClient()` - Redis客户端
|
||||
- 邮件管理器:`email.MustGetService()` - 邮件服务
|
||||
- 存储管理器:`storage.MustGetClient()` - 对象存储客户端
|
||||
- 配置管理器:`config.MustGetConfig()` - 应用配置
|
||||
|
||||
### 技术特性
|
||||
|
||||
- **架构优势**:
|
||||
- 面向过程的函数式设计,代码简洁清晰
|
||||
- 单例管理器模式,线程安全的依赖管理
|
||||
- 按需获取依赖,避免链式传递
|
||||
- 自动数据库迁移(AutoMigrate)
|
||||
|
||||
- **安全性**:
|
||||
- bcrypt密码加密、JWT令牌认证
|
||||
- 邮箱验证码(注册/重置密码/更换邮箱)
|
||||
- Casbin RBAC权限控制
|
||||
- 频率限制(防暴力破解)
|
||||
|
||||
- **性能**:
|
||||
- jsoniter 高性能JSON序列化(替代标准库)
|
||||
- PostgreSQL索引优化
|
||||
- Redis缓存(验证码、会话等)
|
||||
- 预签名URL减轻服务器压力
|
||||
- 连接池管理
|
||||
|
||||
- **可靠性**:
|
||||
- 事务保证数据一致性
|
||||
- 完整的错误处理和日志记录
|
||||
- 优雅关闭和资源清理
|
||||
- 对象存储连接失败时服务仍可启动
|
||||
|
||||
- **可扩展**:
|
||||
- 清晰的函数式架构
|
||||
- 管理器模式统一管理依赖
|
||||
- 环境变量配置(便于容器化)
|
||||
|
||||
- **审计**:
|
||||
- 登录日志(成功/失败)
|
||||
- 操作审计
|
||||
- 下载记录
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 代码结构
|
||||
|
||||
- `cmd/server/` - 应用入口,初始化服务
|
||||
- `internal/handler/` - HTTP请求处理
|
||||
- `internal/service/` - 业务逻辑实现
|
||||
- `internal/repository/` - 数据库操作
|
||||
- `internal/model/` - 数据模型定义
|
||||
- `internal/types/` - 请求/响应类型定义
|
||||
- `internal/middleware/` - 中间件(JWT、CORS、日志等)
|
||||
- `pkg/` - 可复用的公共库
|
||||
|
||||
### 开发规范
|
||||
|
||||
1. **代码风格**: 遵循Go官方代码规范,使用 `gofmt` 格式化
|
||||
2. **架构模式**: 使用函数式设计,避免不必要的结构体和方法
|
||||
3. **依赖管理**: 通过管理器函数获取依赖(如 `database.MustGetDB()`),避免链式传递
|
||||
4. **错误处理**: 使用统一的错误响应格式 (`model.NewErrorResponse`)
|
||||
5. **日志记录**: 使用 Zap 结构化日志,通过 `logger.MustGetLogger()` 获取实例
|
||||
6. **JSON序列化**: 使用 jsoniter 替代标准库 json,提升性能
|
||||
7. **RESTful API**: 遵循 REST 设计原则,合理使用HTTP方法
|
||||
|
||||
### 添加新功能
|
||||
|
||||
1. 在 `internal/model/` 定义数据模型(GORM会自动迁移)
|
||||
2. 在 `internal/repository/` 实现数据访问函数(使用 `database.MustGetDB()` 获取数据库)
|
||||
3. 在 `internal/service/` 实现业务逻辑函数(按需使用管理器获取依赖)
|
||||
4. 在 `internal/handler/` 实现HTTP处理函数(使用管理器获取logger、jwtService等)
|
||||
5. 在 `internal/handler/routes.go` 注册路由
|
||||
|
||||
**示例**:
|
||||
```go
|
||||
// Repository层
|
||||
func FindUserByID(id uint) (*model.User, error) {
|
||||
db := database.MustGetDB()
|
||||
var user model.User
|
||||
err := db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
// Service层
|
||||
func GetUserProfile(userID uint) (*model.User, error) {
|
||||
logger := logger.MustGetLogger()
|
||||
user, err := repository.FindUserByID(userID)
|
||||
if err != nil {
|
||||
logger.Error("获取用户失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Handler层
|
||||
func GetUserProfile(c *gin.Context) {
|
||||
logger := logger.MustGetLogger()
|
||||
jwtService := auth.MustGetJWTService()
|
||||
// ... 处理逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
### 本地开发
|
||||
## 🧪 常用命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
go mod download
|
||||
# 运行单元测试
|
||||
go test ./...
|
||||
|
||||
# 配置环境变量(创建.env文件或直接export)
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件
|
||||
# 重新生成 swagger
|
||||
swag init -g cmd/server/main.go -o docs
|
||||
|
||||
# 启动服务
|
||||
# 方式1: 使用启动脚本
|
||||
./start.sh # Linux/Mac
|
||||
start.bat # Windows
|
||||
|
||||
# 方式2: 直接运行
|
||||
go run cmd/server/main.go
|
||||
# 代码格式化 / 静态检查
|
||||
gofmt -w .
|
||||
golangci-lint run (若已安装)
|
||||
```
|
||||
|
||||
**首次启动**:
|
||||
- 会自动执行数据库迁移(AutoMigrate),创建所有表结构
|
||||
- 如果对象存储未配置,会记录警告但服务仍可启动
|
||||
- 检查日志确认所有服务初始化成功
|
||||
## 🧱 架构说明
|
||||
|
||||
### 生产部署
|
||||
- **分层设计**:Handler -> Service -> Repository -> Model,层次清晰、职责单一。
|
||||
- **依赖管理器**:`pkg/*/manager.go` 使用 `sync.Once` 实现线程安全单例(DB / Redis / Logger / Storage / Email / Auth / Config)。
|
||||
- **Swagger 注解**:所有 Handler、模型、DTO 均补齐 `@Summary` / `@Description` / `@Success`,可直接生成 OpenAPI 文档。
|
||||
- **配置优先级**:`.env` -> 系统环境变量,所有配置均可通过容器注入。
|
||||
- **自动任务**:`internal/task` 承载后台作业,可按需扩展。
|
||||
|
||||
```bash
|
||||
# 构建二进制文件
|
||||
go build -o carrotskin-server cmd/server/main.go
|
||||
## 📝 Swagger 说明
|
||||
|
||||
# 运行服务
|
||||
./carrotskin-server
|
||||
```
|
||||
- `start.sh` 会在启动前执行 `swag init -g cmd/server/main.go -o docs`
|
||||
- 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步
|
||||
- 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露
|
||||
|
||||
### Docker部署
|
||||
## 🤝 贡献指南
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t carrotskin-backend:latest .
|
||||
1. Fork & Clone
|
||||
2. 创建特性分支:`git checkout -b feature/xxx`
|
||||
3. 编写代码并补全测试 / Swagger 注释
|
||||
4. 提交时附上变更说明
|
||||
|
||||
# 启动服务
|
||||
docker-compose up -d
|
||||
```
|
||||
## 📄 许可证
|
||||
|
||||
## 故障排查
|
||||
该项目未附带开源许可证,默认保留所有权利。若需对外使用,请先与作者确认协议。
|
||||
|
||||
### 常见问题
|
||||
---
|
||||
|
||||
1. **数据库连接失败**
|
||||
- 检查 `.env` 中的数据库配置(`DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`)
|
||||
- 确认PostgreSQL服务已启动
|
||||
- 验证数据库用户权限
|
||||
- 确认数据库已创建:`createdb carrotskin` 或 `psql -c "CREATE DATABASE carrotskin;"`
|
||||
- 检查数据库迁移日志,确认表结构创建成功
|
||||
|
||||
2. **Redis连接失败**
|
||||
- 检查Redis服务是否运行:`redis-cli ping`
|
||||
- 验证 `.env` 中的Redis配置
|
||||
- 确认Redis密码是否正确
|
||||
- 检查防火墙规则
|
||||
|
||||
3. **RustFS/MinIO连接失败**
|
||||
- 检查存储服务是否运行
|
||||
- 验证访问密钥是否正确(`RUSTFS_ACCESS_KEY`, `RUSTFS_SECRET_KEY`)
|
||||
- 确认存储桶是否已创建(`RUSTFS_BUCKET_TEXTURES`, `RUSTFS_BUCKET_AVATARS`)
|
||||
- 检查网络连接和端口(`RUSTFS_ENDPOINT`)
|
||||
- **注意**: 如果对象存储连接失败,服务仍可启动,但上传功能不可用
|
||||
|
||||
4. **邮件发送失败**
|
||||
- 检查 `EMAIL_ENABLED=true`
|
||||
- 验证SMTP服务器地址和端口
|
||||
- 确认邮箱用户名和密码正确
|
||||
- 检查邮件服务商是否需要开启SMTP
|
||||
- 查看日志获取详细错误信息
|
||||
|
||||
5. **验证码相关问题**
|
||||
- 验证码过期(10分钟有效期)
|
||||
- 发送过于频繁(1分钟限制)
|
||||
- Redis存储失败(检查Redis连接)
|
||||
- 邮件未收到(检查垃圾邮件)
|
||||
|
||||
6. **JWT验证失败**
|
||||
- 检查 `JWT_SECRET` 是否配置
|
||||
- 验证令牌是否过期(默认168小时)
|
||||
- 确认请求头中包含 `Authorization: Bearer <token>`
|
||||
- Token格式是否正确
|
||||
|
||||
### 调试技巧
|
||||
|
||||
1. **查看日志**
|
||||
```bash
|
||||
# 实时查看日志
|
||||
tail -f logs/app.log
|
||||
|
||||
# 搜索错误日志
|
||||
grep "ERROR" logs/app.log
|
||||
```
|
||||
|
||||
2. **测试Redis连接**
|
||||
```bash
|
||||
redis-cli -h localhost -p 6379 -a your_password
|
||||
> PING
|
||||
> KEYS *
|
||||
```
|
||||
|
||||
3. **测试数据库连接**
|
||||
```bash
|
||||
psql -h localhost -U postgres -d carrotskin
|
||||
\dt # 查看所有表
|
||||
```
|
||||
|
||||
4. **测试邮件配置**
|
||||
- 使用Swagger文档测试 `/api/v1/auth/send-code` 接口
|
||||
- 检查邮件服务商是否限制发送频率
|
||||
|
||||
### 开发调试
|
||||
|
||||
启用详细日志:
|
||||
```bash
|
||||
# 在 .env 中设置
|
||||
LOG_LEVEL=debug
|
||||
SERVER_MODE=debug
|
||||
```
|
||||
如需了解业务细节或 API 调用示例,请参考 `docs/swagger.yaml` 或运行服务后访问 Swagger UI。祝编码愉快!🍀
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
// @title CarrotSkin API
|
||||
// @version 1.0
|
||||
// @description Minecraft皮肤站后端API
|
||||
// @host localhost:8080
|
||||
// @BasePath /api/v1
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -88,12 +97,19 @@ func main() {
|
||||
}
|
||||
emailServiceInstance := email.MustGetService()
|
||||
|
||||
// 初始化Casbin权限服务
|
||||
casbinService, err := auth.NewCasbinService(database.MustGetDB(), cfg.Casbin.ModelPath, loggerInstance)
|
||||
if err != nil {
|
||||
loggerInstance.Fatal("Casbin服务初始化失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 创建依赖注入容器
|
||||
c := container.NewContainer(
|
||||
database.MustGetDB(),
|
||||
redis.MustGetClient(),
|
||||
loggerInstance,
|
||||
auth.MustGetJWTService(),
|
||||
casbinService,
|
||||
storageClient,
|
||||
emailServiceInstance,
|
||||
)
|
||||
|
||||
@@ -11,4 +11,4 @@ g = _, _
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
|
||||
m = g(r.sub, p.sub) && (p.obj == "*" || r.obj == p.obj) && (p.act == "*" || r.act == p.act)
|
||||
|
||||
@@ -12,41 +12,56 @@ services:
|
||||
ports:
|
||||
- "${APP_PORT:-8080}:8080"
|
||||
environment:
|
||||
# 站点配置
|
||||
- SITE_NAME=${SITE_NAME:-CarrotSkin}
|
||||
- SITE_DESCRIPTION=${SITE_DESCRIPTION:-一个优秀的Minecraft皮肤站}
|
||||
- REGISTRATION_ENABLED=${REGISTRATION_ENABLED:-true}
|
||||
- DEFAULT_AVATAR=${DEFAULT_AVATAR:-}
|
||||
# 用户限制配置
|
||||
- MAX_TEXTURES_PER_USER=${MAX_TEXTURES_PER_USER:-50}
|
||||
- MAX_PROFILES_PER_USER=${MAX_PROFILES_PER_USER:-5}
|
||||
# 积分配置
|
||||
- CHECKIN_REWARD=${CHECKIN_REWARD:-10}
|
||||
- TEXTURE_DOWNLOAD_REWARD=${TEXTURE_DOWNLOAD_REWARD:-1}
|
||||
# 服务器配置
|
||||
- SERVER_PORT=8080
|
||||
- SERVER_PORT=:8080
|
||||
- SERVER_MODE=${SERVER_MODE:-release}
|
||||
- SERVER_BASE_PATH=${SERVER_BASE_PATH:-}
|
||||
# 公开访问地址 (用于生成回调URL、邮件链接等)
|
||||
- PUBLIC_URL=${PUBLIC_URL:-http://localhost:8080}
|
||||
- SERVER_SWAGGER_ENABLED=${SERVER_SWAGGER_ENABLED:-true}
|
||||
# 数据库配置
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_USER=carrotskin
|
||||
- DB_PASSWORD=${DB_PASSWORD:-carrotskin123}
|
||||
- DB_NAME=carrotskin
|
||||
- DB_SSLMODE=disable
|
||||
- DATABASE_DRIVER=postgres
|
||||
- DATABASE_HOST=postgres
|
||||
- DATABASE_PORT=5432
|
||||
- DATABASE_USERNAME=carrotskin
|
||||
- DATABASE_PASSWORD=${DATABASE_PASSWORD:-carrotskin123}
|
||||
- DATABASE_NAME=carrotskin
|
||||
- DATABASE_SSL_MODE=disable
|
||||
- DATABASE_TIMEZONE=Asia/Shanghai
|
||||
# Redis 配置
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
- REDIS_DB=0
|
||||
- REDIS_DATABASE=0
|
||||
# JWT 配置
|
||||
- JWT_SECRET=${JWT_SECRET:-your-super-secret-jwt-key-change-in-production}
|
||||
- JWT_EXPIRE_HOURS=24
|
||||
- JWT_EXPIRE_HOURS=168
|
||||
# 存储配置 (RustFS S3兼容)
|
||||
- RUSTFS_ENDPOINT=${RUSTFS_ENDPOINT:-rustfs:9000}
|
||||
- RUSTFS_PUBLIC_URL=${RUSTFS_PUBLIC_URL:-http://localhost:9000}
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin123}
|
||||
- RUSTFS_USE_SSL=${RUSTFS_USE_SSL:-false}
|
||||
- RUSTFS_BUCKET_TEXTURES=${RUSTFS_BUCKET_TEXTURES:-carrotskin}
|
||||
- RUSTFS_BUCKET_AVATARS=${RUSTFS_BUCKET_AVATARS:-carrotskin}
|
||||
# 邮件配置 (可选)
|
||||
- SMTP_HOST=${SMTP_HOST:-}
|
||||
- SMTP_PORT=${SMTP_PORT:-587}
|
||||
- SMTP_USER=${SMTP_USER:-}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD:-}
|
||||
- SMTP_FROM=${SMTP_FROM:-}
|
||||
- RUSTFS_BUCKET_TEXTURES=${RUSTFS_BUCKET_TEXTURES:-carrot-skin-textures}
|
||||
- RUSTFS_BUCKET_AVATARS=${RUSTFS_BUCKET_AVATARS:-carrot-skin-avatars}
|
||||
# 安全配置
|
||||
- SECURITY_ALLOWED_ORIGINS=${SECURITY_ALLOWED_ORIGINS:-*}
|
||||
- SECURITY_ALLOWED_DOMAINS=${SECURITY_ALLOWED_DOMAINS:-localhost,127.0.0.1}
|
||||
# 邮件配置
|
||||
- EMAIL_ENABLED=${EMAIL_ENABLED:-false}
|
||||
- EMAIL_SMTP_HOST=${EMAIL_SMTP_HOST:-}
|
||||
- EMAIL_SMTP_PORT=${EMAIL_SMTP_PORT:-587}
|
||||
- EMAIL_USERNAME=${EMAIL_USERNAME:-}
|
||||
- EMAIL_PASSWORD=${EMAIL_PASSWORD:-}
|
||||
- EMAIL_FROM_NAME=${EMAIL_FROM_NAME:-CarrotSkin}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -68,7 +83,7 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=carrotskin
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-carrotskin123}
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD:-carrotskin123}
|
||||
- POSTGRES_DB=carrotskin
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
@@ -108,7 +123,7 @@ services:
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
|
||||
# ==================== RustFS 对象存储 (可选) ====================
|
||||
# ==================== RustFS 对象存储====================
|
||||
rustfs:
|
||||
image: ghcr.io/rustfs/rustfs:latest
|
||||
container_name: carrotskin-rustfs
|
||||
@@ -148,14 +163,19 @@ services:
|
||||
echo '等待 RustFS 启动...';
|
||||
sleep 5;
|
||||
mc alias set myrustfs http://rustfs:9000 $${RUSTFS_ACCESS_KEY} $${RUSTFS_SECRET_KEY};
|
||||
mc mb myrustfs/$${RUSTFS_BUCKET} --ignore-existing;
|
||||
mc anonymous set download myrustfs/$${RUSTFS_BUCKET};
|
||||
echo '存储桶 $${RUSTFS_BUCKET} 创建完成,已设置公开读取权限';
|
||||
echo '创建材质存储桶...';
|
||||
mc mb myrustfs/$${RUSTFS_BUCKET_TEXTURES} --ignore-existing;
|
||||
mc anonymous set download myrustfs/$${RUSTFS_BUCKET_TEXTURES};
|
||||
echo '创建头像存储桶...';
|
||||
mc mb myrustfs/$${RUSTFS_BUCKET_AVATARS} --ignore-existing;
|
||||
mc anonymous set download myrustfs/$${RUSTFS_BUCKET_AVATARS};
|
||||
echo '存储桶创建完成: $${RUSTFS_BUCKET_TEXTURES}, $${RUSTFS_BUCKET_AVATARS}';
|
||||
"
|
||||
environment:
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin123}
|
||||
- RUSTFS_BUCKET=${RUSTFS_BUCKET_TEXTURES:-carrotskin}
|
||||
- RUSTFS_BUCKET_TEXTURES=${RUSTFS_BUCKET_TEXTURES:-carrot-skin-textures}
|
||||
- RUSTFS_BUCKET_AVATARS=${RUSTFS_BUCKET_AVATARS:-carrot-skin-avatars}
|
||||
networks:
|
||||
- carrotskin-network
|
||||
profiles:
|
||||
|
||||
30
go.mod
30
go.mod
@@ -6,6 +6,7 @@ toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/alicebob/miniredis/v2 v2.31.1
|
||||
github.com/casbin/casbin/v2 v2.123.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
@@ -13,6 +14,8 @@ require (
|
||||
github.com/minio/minio-go/v7 v7.0.97
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/wenlng/go-captcha-assets v1.0.7
|
||||
github.com/wenlng/go-captcha/v2 v2.0.4
|
||||
go.uber.org/zap v1.27.1
|
||||
@@ -24,21 +27,39 @@ require (
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/casbin/govaluate v1.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/glebarez/go-sqlite v1.20.3 // indirect
|
||||
github.com/glebarez/sqlite v1.7.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.7.2 // indirect
|
||||
github.com/minio/crc64nvme v1.1.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.0 // indirect
|
||||
@@ -46,11 +67,19 @@ require (
|
||||
golang.org/x/image v0.33.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gorm.io/driver/mysql v1.6.0 // indirect
|
||||
gorm.io/driver/sqlserver v1.6.0 // indirect
|
||||
gorm.io/plugin/dbresolver v1.6.0 // indirect
|
||||
modernc.org/libc v1.22.2 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.20.3 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/casbin/gorm-adapter/v3 v3.39.0
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -83,6 +112,7 @@ require (
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
|
||||
176
go.sum
176
go.sum
@@ -1,10 +1,35 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
@@ -15,6 +40,12 @@ github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPII
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/casbin/casbin/v2 v2.123.0 h1:UkiMllBgn3MrwHGiZTDFVTV9up+W2CRLufZwKiuAmpA=
|
||||
github.com/casbin/casbin/v2 v2.123.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
|
||||
github.com/casbin/gorm-adapter/v3 v3.39.0 h1:k15txH6vE4796MuA+LFcU8I1vMjutklyzMXfjDz7lzo=
|
||||
github.com/casbin/gorm-adapter/v3 v3.39.0/go.mod h1:kjXoK8MqA3E/CcqEF2l3SCkhJj1YiHVR6SF0LMvJoH4=
|
||||
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
|
||||
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -22,11 +53,14 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -35,12 +69,28 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
|
||||
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
|
||||
github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
|
||||
github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -57,8 +107,12 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
@@ -66,11 +120,21 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -79,6 +143,12 @@ github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
@@ -87,6 +157,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
@@ -96,16 +168,28 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
|
||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
|
||||
@@ -119,10 +203,19 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
@@ -131,6 +224,9 @@ github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQB
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
@@ -150,6 +246,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -159,6 +257,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -185,7 +289,14 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
||||
@@ -193,52 +304,106 @@ golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||
@@ -251,5 +416,16 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gorm.io/plugin/dbresolver v1.6.0 h1:XvKDeOtTn1EIX6s4SrKpEH82q0gXVemhYjbYZFGFVcw=
|
||||
gorm.io/plugin/dbresolver v1.6.0/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
|
||||
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
|
||||
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
|
||||
modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
|
||||
|
||||
@@ -22,6 +22,7 @@ type Container struct {
|
||||
Redis *redis.Client
|
||||
Logger *zap.Logger
|
||||
JWT *auth.JWTService
|
||||
Casbin *auth.CasbinService
|
||||
Storage *storage.StorageClient
|
||||
CacheManager *database.CacheManager
|
||||
|
||||
@@ -30,7 +31,6 @@ type Container struct {
|
||||
ProfileRepo repository.ProfileRepository
|
||||
TextureRepo repository.TextureRepository
|
||||
ClientRepo repository.ClientRepository
|
||||
ConfigRepo repository.SystemConfigRepository
|
||||
YggdrasilRepo repository.YggdrasilRepository
|
||||
|
||||
// Service层
|
||||
@@ -40,7 +40,6 @@ type Container struct {
|
||||
TokenService service.TokenService
|
||||
YggdrasilService service.YggdrasilService
|
||||
VerificationService service.VerificationService
|
||||
UploadService service.UploadService
|
||||
SecurityService service.SecurityService
|
||||
CaptchaService service.CaptchaService
|
||||
SignatureService *service.SignatureService
|
||||
@@ -52,6 +51,7 @@ func NewContainer(
|
||||
redisClient *redis.Client,
|
||||
logger *zap.Logger,
|
||||
jwtService *auth.JWTService,
|
||||
casbinService *auth.CasbinService,
|
||||
storageClient *storage.StorageClient,
|
||||
emailService interface{}, // 接受 email.Service 但使用 interface{} 避免循环依赖
|
||||
) *Container {
|
||||
@@ -75,6 +75,7 @@ func NewContainer(
|
||||
Redis: redisClient,
|
||||
Logger: logger,
|
||||
JWT: jwtService,
|
||||
Casbin: casbinService,
|
||||
Storage: storageClient,
|
||||
CacheManager: cacheManager,
|
||||
}
|
||||
@@ -84,7 +85,6 @@ func NewContainer(
|
||||
c.ProfileRepo = repository.NewProfileRepository(db)
|
||||
c.TextureRepo = repository.NewTextureRepository(db)
|
||||
c.ClientRepo = repository.NewClientRepository(db)
|
||||
c.ConfigRepo = repository.NewSystemConfigRepository(db)
|
||||
c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
|
||||
|
||||
// 初始化SignatureService(作为依赖注入,避免在容器中创建并立即调用)
|
||||
@@ -92,7 +92,7 @@ func NewContainer(
|
||||
c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger)
|
||||
|
||||
// 初始化Service(注入缓存管理器)
|
||||
c.UserService = service.NewUserService(c.UserRepo, c.ConfigRepo, jwtService, redisClient, cacheManager, logger)
|
||||
c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
|
||||
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
|
||||
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger)
|
||||
|
||||
@@ -125,7 +125,6 @@ func NewContainer(
|
||||
|
||||
// 初始化其他服务
|
||||
c.SecurityService = service.NewSecurityService(redisClient)
|
||||
c.UploadService = service.NewUploadService(storageClient)
|
||||
c.CaptchaService = service.NewCaptchaService(redisClient, logger)
|
||||
|
||||
// 初始化VerificationService(需要email.Service)
|
||||
@@ -206,13 +205,6 @@ func WithTextureRepo(repo repository.TextureRepository) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfigRepo 设置系统配置仓储
|
||||
func WithConfigRepo(repo repository.SystemConfigRepository) Option {
|
||||
return func(c *Container) {
|
||||
c.ConfigRepo = repo
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserService 设置用户服务
|
||||
func WithUserService(svc service.UserService) Option {
|
||||
return func(c *Container) {
|
||||
@@ -262,13 +254,6 @@ func WithVerificationService(svc service.VerificationService) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithUploadService 设置上传服务
|
||||
func WithUploadService(svc service.UploadService) Option {
|
||||
return func(c *Container) {
|
||||
c.UploadService = svc
|
||||
}
|
||||
}
|
||||
|
||||
// WithSecurityService 设置安全服务
|
||||
func WithSecurityService(svc service.SecurityService) Option {
|
||||
return func(c *Container) {
|
||||
|
||||
382
internal/handler/admin_handler.go
Normal file
382
internal/handler/admin_handler.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AdminHandler 管理员处理器
|
||||
type AdminHandler struct {
|
||||
container *container.Container
|
||||
}
|
||||
|
||||
// NewAdminHandler 创建管理员处理器
|
||||
func NewAdminHandler(c *container.Container) *AdminHandler {
|
||||
return &AdminHandler{container: c}
|
||||
}
|
||||
|
||||
// SetUserRoleRequest 设置用户角色请求
|
||||
type SetUserRoleRequest struct {
|
||||
UserID int64 `json:"user_id" binding:"required"`
|
||||
Role string `json:"role" binding:"required,oneof=user admin"`
|
||||
}
|
||||
|
||||
// SetUserRole 设置用户角色
|
||||
// @Summary 设置用户角色
|
||||
// @Description 管理员设置指定用户的角色
|
||||
// @Tags Admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body SetUserRoleRequest true "设置角色请求"
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/admin/users/role [put]
|
||||
func (h *AdminHandler) SetUserRole(c *gin.Context) {
|
||||
var req SetUserRoleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
RespondBadRequest(c, "参数错误", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前操作者ID
|
||||
operatorID, _ := c.Get("user_id")
|
||||
|
||||
// 不能修改自己的角色
|
||||
if req.UserID == operatorID.(int64) {
|
||||
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
||||
model.CodeBadRequest,
|
||||
"不能修改自己的角色",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查目标用户是否存在
|
||||
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
||||
if err != nil || targetUser == nil {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
"用户不存在",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户角色
|
||||
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
|
||||
"role": req.Role,
|
||||
})
|
||||
if err != nil {
|
||||
RespondServerError(c, "更新用户角色失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.container.Logger.Info("管理员修改用户角色",
|
||||
zap.Int64("operator_id", operatorID.(int64)),
|
||||
zap.Int64("target_user_id", req.UserID),
|
||||
zap.String("new_role", req.Role),
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"message": "用户角色更新成功",
|
||||
"user_id": req.UserID,
|
||||
"role": req.Role,
|
||||
}))
|
||||
}
|
||||
|
||||
// GetUserList 获取用户列表
|
||||
// @Summary 获取用户列表
|
||||
// @Description 管理员获取所有用户列表
|
||||
// @Tags Admin
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/admin/users [get]
|
||||
func (h *AdminHandler) GetUserList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 使用数据库直接查询用户列表
|
||||
var users []model.User
|
||||
var total int64
|
||||
|
||||
db := h.container.DB
|
||||
db.Model(&model.User{}).Count(&total)
|
||||
db.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&users)
|
||||
|
||||
// 构建响应(隐藏敏感信息)
|
||||
userList := make([]gin.H, len(users))
|
||||
for i, u := range users {
|
||||
userList[i] = gin.H{
|
||||
"id": u.ID,
|
||||
"username": u.Username,
|
||||
"email": u.Email,
|
||||
"avatar": u.Avatar,
|
||||
"role": u.Role,
|
||||
"status": u.Status,
|
||||
"points": u.Points,
|
||||
"last_login_at": u.LastLoginAt,
|
||||
"created_at": u.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"users": userList,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}))
|
||||
}
|
||||
|
||||
// GetUserDetail 获取用户详情
|
||||
// @Summary 获取用户详情
|
||||
// @Description 管理员获取指定用户的详细信息
|
||||
// @Tags Admin
|
||||
// @Produce json
|
||||
// @Param id path int true "用户ID"
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/admin/users/{id} [get]
|
||||
func (h *AdminHandler) GetUserDetail(c *gin.Context) {
|
||||
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
RespondBadRequest(c, "无效的用户ID", err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.container.UserRepo.FindByID(c.Request.Context(), userID)
|
||||
if err != nil || user == nil {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
"用户不存在",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
"points": user.Points,
|
||||
"properties": user.Properties,
|
||||
"last_login_at": user.LastLoginAt,
|
||||
"created_at": user.CreatedAt,
|
||||
"updated_at": user.UpdatedAt,
|
||||
}))
|
||||
}
|
||||
|
||||
// SetUserStatusRequest 设置用户状态请求
|
||||
type SetUserStatusRequest struct {
|
||||
UserID int64 `json:"user_id" binding:"required"`
|
||||
Status int16 `json:"status" binding:"required,oneof=1 0 -1"` // 1:正常, 0:禁用, -1:删除
|
||||
}
|
||||
|
||||
// SetUserStatus 设置用户状态
|
||||
// @Summary 设置用户状态
|
||||
// @Description 管理员设置用户状态(启用/禁用)
|
||||
// @Tags Admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body SetUserStatusRequest true "设置状态请求"
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/admin/users/status [put]
|
||||
func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
||||
var req SetUserStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
RespondBadRequest(c, "参数错误", err)
|
||||
return
|
||||
}
|
||||
|
||||
operatorID, _ := c.Get("user_id")
|
||||
|
||||
// 不能修改自己的状态
|
||||
if req.UserID == operatorID.(int64) {
|
||||
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
||||
model.CodeBadRequest,
|
||||
"不能修改自己的状态",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查目标用户是否存在
|
||||
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
||||
if err != nil || targetUser == nil {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
"用户不存在",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
|
||||
"status": req.Status,
|
||||
})
|
||||
if err != nil {
|
||||
RespondServerError(c, "更新用户状态失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
statusText := map[int16]string{1: "正常", 0: "禁用", -1: "删除"}[req.Status]
|
||||
|
||||
h.container.Logger.Info("管理员修改用户状态",
|
||||
zap.Int64("operator_id", operatorID.(int64)),
|
||||
zap.Int64("target_user_id", req.UserID),
|
||||
zap.Int16("new_status", req.Status),
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"message": "用户状态更新成功",
|
||||
"user_id": req.UserID,
|
||||
"status": req.Status,
|
||||
"status_text": statusText,
|
||||
}))
|
||||
}
|
||||
|
||||
// DeleteTexture 管理员删除材质
|
||||
// @Summary 管理员删除材质
|
||||
// @Description 管理员可以删除任意材质(用于审核不当内容)
|
||||
// @Tags Admin
|
||||
// @Produce json
|
||||
// @Param id path int true "材质ID"
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
|
||||
// @Failure 404 {object} model.ErrorResponse "材质不存在"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/admin/textures/{id} [delete]
|
||||
func (h *AdminHandler) DeleteTexture(c *gin.Context) {
|
||||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
RespondBadRequest(c, "无效的材质ID", err)
|
||||
return
|
||||
}
|
||||
|
||||
operatorID, _ := c.Get("user_id")
|
||||
|
||||
// 检查材质是否存在
|
||||
var texture model.Texture
|
||||
if err := h.container.DB.First(&texture, textureID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
"材质不存在",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 删除材质
|
||||
if err := h.container.DB.Delete(&texture).Error; err != nil {
|
||||
RespondServerError(c, "删除材质失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.container.Logger.Info("管理员删除材质",
|
||||
zap.Int64("operator_id", operatorID.(int64)),
|
||||
zap.Int64("texture_id", textureID),
|
||||
zap.Int64("uploader_id", texture.UploaderID),
|
||||
zap.String("texture_name", texture.Name),
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"message": "材质删除成功",
|
||||
"texture_id": textureID,
|
||||
}))
|
||||
}
|
||||
|
||||
// GetTextureList 管理员获取材质列表
|
||||
// @Summary 管理员获取材质列表
|
||||
// @Description 管理员获取所有材质列表(用于审核)
|
||||
// @Tags Admin
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/admin/textures [get]
|
||||
func (h *AdminHandler) GetTextureList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var textures []model.Texture
|
||||
var total int64
|
||||
|
||||
db := h.container.DB
|
||||
db.Model(&model.Texture{}).Count(&total)
|
||||
db.Preload("Uploader").Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&textures)
|
||||
|
||||
// 构建响应
|
||||
textureList := make([]gin.H, len(textures))
|
||||
for i, t := range textures {
|
||||
uploaderName := ""
|
||||
if t.Uploader != nil {
|
||||
uploaderName = t.Uploader.Username
|
||||
}
|
||||
textureList[i] = gin.H{
|
||||
"id": t.ID,
|
||||
"name": t.Name,
|
||||
"type": t.Type,
|
||||
"hash": t.Hash,
|
||||
"uploader_id": t.UploaderID,
|
||||
"uploader_name": uploaderName,
|
||||
"is_public": t.IsPublic,
|
||||
"download_count": t.DownloadCount,
|
||||
"created_at": t.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"textures": textureList,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}))
|
||||
}
|
||||
|
||||
// GetPermissions 获取权限列表
|
||||
// @Summary 获取权限列表
|
||||
// @Description 管理员获取所有Casbin权限规则
|
||||
// @Tags Admin
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/admin/permissions [get]
|
||||
func (h *AdminHandler) GetPermissions(c *gin.Context) {
|
||||
// 获取所有权限规则
|
||||
policies, _ := h.container.Casbin.GetEnforcer().GetPolicy()
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"policies": policies,
|
||||
}))
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func NewAuthHandler(c *container.Container) *AuthHandler {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body types.RegisterRequest true "注册信息"
|
||||
// @Success 200 {object} model.Response "注册成功"
|
||||
// @Success 200 {object} model.Response{data=types.LoginResponse} "注册成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "请求参数错误"
|
||||
// @Router /api/v1/auth/register [post]
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
@@ -107,7 +107,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body types.SendVerificationCodeRequest true "发送验证码请求"
|
||||
// @Success 200 {object} model.Response "发送成功"
|
||||
// @Success 200 {object} model.Response{data=map[string]string} "发送成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "请求参数错误"
|
||||
// @Router /api/v1/auth/send-code [post]
|
||||
func (h *AuthHandler) SendVerificationCode(c *gin.Context) {
|
||||
@@ -137,7 +137,7 @@ func (h *AuthHandler) SendVerificationCode(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body types.ResetPasswordRequest true "重置密码请求"
|
||||
// @Success 200 {object} model.Response "重置成功"
|
||||
// @Success 200 {object} model.Response{data=map[string]string} "重置成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "请求参数错误"
|
||||
// @Router /api/v1/auth/reset-password [post]
|
||||
func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
||||
|
||||
@@ -34,7 +34,7 @@ type CaptchaVerifyRequest struct {
|
||||
// @Tags captcha
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "生成成功"
|
||||
// @Success 200 {object} map[string]interface{} "生成成功 {code: 200, data: {masterImage, tileImage, captchaId, y}}"
|
||||
// @Failure 500 {object} map[string]interface{} "生成失败"
|
||||
// @Router /api/v1/captcha/generate [get]
|
||||
func (h *CaptchaHandler) Generate(c *gin.Context) {
|
||||
@@ -66,7 +66,7 @@ func (h *CaptchaHandler) Generate(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CaptchaVerifyRequest true "验证请求"
|
||||
// @Success 200 {object} map[string]interface{} "验证结果"
|
||||
// @Success 200 {object} map[string]interface{} "验证结果 {code: 200/400, msg: string}"
|
||||
// @Failure 400 {object} map[string]interface{} "参数错误"
|
||||
// @Router /api/v1/captcha/verify [post]
|
||||
func (h *CaptchaHandler) Verify(c *gin.Context) {
|
||||
|
||||
@@ -35,7 +35,16 @@ type CustomSkinAPIResponse struct {
|
||||
}
|
||||
|
||||
// GetPlayerInfo 获取玩家信息
|
||||
// GET {ROOT}/{USERNAME}.json
|
||||
// @Summary 获取玩家信息
|
||||
// @Description CustomSkinAPI: 获取玩家皮肤配置信息
|
||||
// @Tags CustomSkinAPI
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username path string true "玩家用户名"
|
||||
// @Success 200 {object} CustomSkinAPIResponse
|
||||
// @Failure 400 {object} map[string]string "用户名不能为空"
|
||||
// @Failure 404 {object} map[string]string "玩家未找到"
|
||||
// @Router /api/v1/csl/{username} [get]
|
||||
func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
if username == "" {
|
||||
@@ -136,7 +145,14 @@ func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetTexture 获取资源文件
|
||||
// GET {ROOT}/textures/{hash}
|
||||
// @Summary 获取资源文件
|
||||
// @Description CustomSkinAPI: 获取材质图片文件
|
||||
// @Tags CustomSkinAPI
|
||||
// @Param hash path string true "材质Hash"
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 400 {object} map[string]string "资源标识符不能为空"
|
||||
// @Failure 404 {object} map[string]string "资源未找到或不可用"
|
||||
// @Router /api/v1/csl/textures/{hash} [get]
|
||||
func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
|
||||
hash := c.Param("hash")
|
||||
if hash == "" {
|
||||
|
||||
@@ -62,6 +62,19 @@ func UserToUserInfo(user *model.User) *types.UserInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// UserToPublicUserInfo 将 User 模型转换为 PublicUserInfo 响应
|
||||
func UserToPublicUserInfo(user *model.User) *types.PublicUserInfo {
|
||||
return &types.PublicUserInfo{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Avatar: user.Avatar,
|
||||
Points: user.Points,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ProfileToProfileInfo 将 Profile 模型转换为 ProfileInfo 响应
|
||||
func ProfileToProfileInfo(profile *model.Profile) *types.ProfileInfo {
|
||||
return &types.ProfileInfo{
|
||||
@@ -70,7 +83,6 @@ func ProfileToProfileInfo(profile *model.Profile) *types.ProfileInfo {
|
||||
Name: profile.Name,
|
||||
SkinID: profile.SkinID,
|
||||
CapeID: profile.CapeID,
|
||||
IsActive: profile.IsActive,
|
||||
LastUsedAt: profile.LastUsedAt,
|
||||
CreatedAt: profile.CreatedAt,
|
||||
UpdatedAt: profile.UpdatedAt,
|
||||
@@ -88,9 +100,15 @@ func ProfilesToProfileInfos(profiles []*model.Profile) []*types.ProfileInfo {
|
||||
|
||||
// TextureToTextureInfo 将 Texture 模型转换为 TextureInfo 响应
|
||||
func TextureToTextureInfo(texture *model.Texture) *types.TextureInfo {
|
||||
uploaderUsername := ""
|
||||
if texture.Uploader != nil {
|
||||
uploaderUsername = texture.Uploader.Username
|
||||
}
|
||||
|
||||
return &types.TextureInfo{
|
||||
ID: texture.ID,
|
||||
UploaderID: texture.UploaderID,
|
||||
UploaderUsername: uploaderUsername,
|
||||
Name: texture.Name,
|
||||
Description: texture.Description,
|
||||
Type: types.TextureType(texture.Type),
|
||||
|
||||
@@ -72,7 +72,8 @@ func (h *ProfileHandler) Create(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} model.Response "获取成功"
|
||||
// @Success 200 {object} model.Response{data=[]types.ProfileInfo} "获取成功"
|
||||
// @Failure 500 {object} model.ErrorResponse "服务器错误"
|
||||
// @Router /api/v1/profile [get]
|
||||
func (h *ProfileHandler) List(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
@@ -100,7 +101,7 @@ func (h *ProfileHandler) List(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param uuid path string true "档案UUID"
|
||||
// @Success 200 {object} model.Response "获取成功"
|
||||
// @Success 200 {object} model.Response{data=types.ProfileInfo} "获取成功"
|
||||
// @Failure 404 {object} model.ErrorResponse "档案不存在"
|
||||
// @Router /api/v1/profile/{uuid} [get]
|
||||
func (h *ProfileHandler) Get(c *gin.Context) {
|
||||
@@ -132,7 +133,7 @@ func (h *ProfileHandler) Get(c *gin.Context) {
|
||||
// @Security BearerAuth
|
||||
// @Param uuid path string true "档案UUID"
|
||||
// @Param request body types.UpdateProfileRequest true "更新信息"
|
||||
// @Success 200 {object} model.Response "更新成功"
|
||||
// @Success 200 {object} model.Response{data=types.ProfileInfo} "更新成功"
|
||||
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
||||
// @Router /api/v1/profile/{uuid} [put]
|
||||
func (h *ProfileHandler) Update(c *gin.Context) {
|
||||
@@ -180,7 +181,7 @@ func (h *ProfileHandler) Update(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param uuid path string true "档案UUID"
|
||||
// @Success 200 {object} model.Response "删除成功"
|
||||
// @Success 200 {object} model.Response{data=map[string]string} "删除成功"
|
||||
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
||||
// @Router /api/v1/profile/{uuid} [delete]
|
||||
func (h *ProfileHandler) Delete(c *gin.Context) {
|
||||
@@ -207,39 +208,3 @@ func (h *ProfileHandler) Delete(c *gin.Context) {
|
||||
|
||||
RespondSuccess(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
// SetActive 设置活跃档案
|
||||
// @Summary 设置活跃档案
|
||||
// @Description 将指定档案设置为活跃状态
|
||||
// @Tags profile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param uuid path string true "档案UUID"
|
||||
// @Success 200 {object} model.Response "设置成功"
|
||||
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
||||
// @Router /api/v1/profile/{uuid}/activate [post]
|
||||
func (h *ProfileHandler) SetActive(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
uuid := c.Param("uuid")
|
||||
if uuid == "" {
|
||||
RespondBadRequest(c, "UUID不能为空", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.container.ProfileService.SetActive(c.Request.Context(), uuid, userID); err != nil {
|
||||
h.logger.Error("设置活跃档案失败",
|
||||
zap.String("uuid", uuid),
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
RespondWithError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
RespondSuccess(c, gin.H{"message": "设置成功"})
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package handler
|
||||
import (
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/middleware"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/auth"
|
||||
"carrotskin/pkg/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// Handlers 集中管理所有Handler
|
||||
@@ -18,6 +20,7 @@ type Handlers struct {
|
||||
Captcha *CaptchaHandler
|
||||
Yggdrasil *YggdrasilHandler
|
||||
CustomSkin *CustomSkinHandler
|
||||
Admin *AdminHandler
|
||||
}
|
||||
|
||||
// NewHandlers 创建所有Handler实例
|
||||
@@ -30,6 +33,7 @@ func NewHandlers(c *container.Container) *Handlers {
|
||||
Captcha: NewCaptchaHandler(c),
|
||||
Yggdrasil: NewYggdrasilHandler(c),
|
||||
CustomSkin: NewCustomSkinHandler(c),
|
||||
Admin: NewAdminHandler(c),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +42,12 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
// 健康检查路由
|
||||
router.GET("/health", HealthCheck)
|
||||
|
||||
// Swagger文档路由
|
||||
cfg, _ := config.GetConfig()
|
||||
if cfg != nil && cfg.Server.SwaggerEnabled {
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
|
||||
// 创建Handler实例
|
||||
h := NewHandlers(c)
|
||||
|
||||
@@ -62,11 +72,11 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
// Yggdrasil API路由组
|
||||
registerYggdrasilRoutesWithDI(v1, h.Yggdrasil)
|
||||
|
||||
// 系统路由
|
||||
registerSystemRoutes(v1)
|
||||
|
||||
// CustomSkinAPI 路由
|
||||
registerCustomSkinRoutes(v1, h.CustomSkin)
|
||||
|
||||
// 管理员路由(需要管理员权限)
|
||||
registerAdminRoutes(v1, c, h.Admin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +93,10 @@ func registerAuthRoutes(v1 *gin.RouterGroup, h *AuthHandler) {
|
||||
|
||||
// registerUserRoutes 注册用户路由
|
||||
func registerUserRoutes(v1 *gin.RouterGroup, h *UserHandler, jwtService *auth.JWTService) {
|
||||
// 公开用户信息路由(无需认证)
|
||||
v1.GET("/users/public", h.GetPublicInfo)
|
||||
|
||||
// 需要认证的用户路由
|
||||
userGroup := v1.Group("/user")
|
||||
userGroup.Use(middleware.AuthMiddleware(jwtService))
|
||||
{
|
||||
@@ -90,8 +104,8 @@ func registerUserRoutes(v1 *gin.RouterGroup, h *UserHandler, jwtService *auth.JW
|
||||
userGroup.PUT("/profile", h.UpdateProfile)
|
||||
|
||||
// 头像相关
|
||||
userGroup.POST("/avatar/upload-url", h.GenerateAvatarUploadURL)
|
||||
userGroup.PUT("/avatar", h.UpdateAvatar)
|
||||
userGroup.POST("/avatar/upload", h.UploadAvatar) // 直接上传头像文件
|
||||
userGroup.PUT("/avatar", h.UpdateAvatar) // 更新头像URL(外部URL)
|
||||
|
||||
// 更换邮箱
|
||||
userGroup.POST("/change-email", h.ChangeEmail)
|
||||
@@ -114,8 +128,6 @@ func registerTextureRoutes(v1 *gin.RouterGroup, h *TextureHandler, jwtService *a
|
||||
textureAuth.Use(middleware.AuthMiddleware(jwtService))
|
||||
{
|
||||
textureAuth.POST("/upload", h.Upload) // 直接上传文件
|
||||
textureAuth.POST("/upload-url", h.GenerateUploadURL) // 生成预签名URL(保留兼容性)
|
||||
textureAuth.POST("", h.Create) // 创建材质记录(配合预签名URL使用)
|
||||
textureAuth.PUT("/:id", h.Update)
|
||||
textureAuth.DELETE("/:id", h.Delete)
|
||||
textureAuth.POST("/:id/favorite", h.ToggleFavorite)
|
||||
@@ -143,7 +155,6 @@ func registerProfileRoutesWithDI(v1 *gin.RouterGroup, h *ProfileHandler, jwtServ
|
||||
profileAuth.GET("/", h.List)
|
||||
profileAuth.PUT("/:uuid", h.Update)
|
||||
profileAuth.DELETE("/:uuid", h.Delete)
|
||||
profileAuth.POST("/:uuid/activate", h.SetActive)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,20 +196,25 @@ func registerYggdrasilRoutesWithDI(v1 *gin.RouterGroup, h *YggdrasilHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
// registerSystemRoutes 注册系统路由
|
||||
func registerSystemRoutes(v1 *gin.RouterGroup) {
|
||||
system := v1.Group("/system")
|
||||
// registerAdminRoutes 注册管理员路由
|
||||
func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHandler) {
|
||||
admin := v1.Group("/admin")
|
||||
admin.Use(middleware.AuthMiddleware(c.JWT))
|
||||
admin.Use(middleware.RequireAdmin())
|
||||
{
|
||||
system.GET("/config", func(c *gin.Context) {
|
||||
// TODO: 实现从数据库读取系统配置
|
||||
c.JSON(200, model.NewSuccessResponse(gin.H{
|
||||
"site_name": "CarrotSkin",
|
||||
"site_description": "A Minecraft Skin Station",
|
||||
"registration_enabled": true,
|
||||
"max_textures_per_user": 100,
|
||||
"max_profiles_per_user": 5,
|
||||
}))
|
||||
})
|
||||
|
||||
// 用户管理
|
||||
admin.GET("/users", h.GetUserList)
|
||||
admin.GET("/users/:id", h.GetUserDetail)
|
||||
admin.PUT("/users/role", h.SetUserRole)
|
||||
admin.PUT("/users/status", h.SetUserStatus)
|
||||
|
||||
// 材质管理(审核)
|
||||
admin.GET("/textures", h.GetTextureList)
|
||||
admin.DELETE("/textures/:id", h.DeleteTexture)
|
||||
|
||||
// 权限管理
|
||||
admin.GET("/permissions", h.GetPermissions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,94 +24,17 @@ func NewTextureHandler(c *container.Container) *TextureHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateUploadURL 生成材质上传URL
|
||||
func (h *TextureHandler) GenerateUploadURL(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req types.GenerateTextureUploadURLRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
RespondBadRequest(c, "请求参数错误", err)
|
||||
return
|
||||
}
|
||||
|
||||
if h.container.Storage == nil {
|
||||
RespondServerError(c, "存储服务不可用", nil)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.container.UploadService.GenerateTextureUploadURL(
|
||||
c.Request.Context(),
|
||||
userID,
|
||||
req.FileName,
|
||||
string(req.TextureType),
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("生成材质上传URL失败",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("file_name", req.FileName),
|
||||
zap.String("texture_type", string(req.TextureType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
RespondBadRequest(c, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
RespondSuccess(c, &types.GenerateTextureUploadURLResponse{
|
||||
PostURL: result.PostURL,
|
||||
FormData: result.FormData,
|
||||
TextureURL: result.FileURL,
|
||||
ExpiresIn: 900,
|
||||
})
|
||||
}
|
||||
|
||||
// Create 创建材质记录
|
||||
func (h *TextureHandler) Create(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req types.CreateTextureRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
RespondBadRequest(c, "请求参数错误", err)
|
||||
return
|
||||
}
|
||||
|
||||
maxTextures := h.container.UserService.GetMaxTexturesPerUser()
|
||||
if err := h.container.TextureService.CheckUploadLimit(c.Request.Context(), userID, maxTextures); err != nil {
|
||||
RespondBadRequest(c, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
texture, err := h.container.TextureService.Create(
|
||||
c.Request.Context(),
|
||||
userID,
|
||||
req.Name,
|
||||
req.Description,
|
||||
string(req.Type),
|
||||
req.URL,
|
||||
req.Hash,
|
||||
req.Size,
|
||||
req.IsPublic,
|
||||
req.IsSlim,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("创建材质失败",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("name", req.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
RespondBadRequest(c, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
RespondSuccess(c, TextureToTextureInfo(texture))
|
||||
}
|
||||
|
||||
// Get 获取材质详情
|
||||
// @Summary 获取材质详情
|
||||
// @Description 获取指定ID的材质详细信息
|
||||
// @Tags texture
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "材质ID"
|
||||
// @Success 200 {object} model.Response{data=types.TextureInfo} "获取成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 404 {object} model.ErrorResponse "材质不存在"
|
||||
// @Router /api/v1/texture/{id} [get]
|
||||
func (h *TextureHandler) Get(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -129,6 +52,19 @@ func (h *TextureHandler) Get(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Search 搜索材质
|
||||
// @Summary 搜索材质
|
||||
// @Description 搜索材质列表,支持关键词、类型、公开性筛选和分页
|
||||
// @Tags texture
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param keyword query string false "关键词"
|
||||
// @Param type query string false "材质类型 (SKIN/CAPE)"
|
||||
// @Param public_only query boolean false "仅显示公开材质"
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||
// @Failure 500 {object} model.ErrorResponse "服务器错误"
|
||||
// @Router /api/v1/texture [get]
|
||||
func (h *TextureHandler) Search(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
textureTypeStr := c.Query("type")
|
||||
@@ -172,6 +108,18 @@ func (h *TextureHandler) Search(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Update 更新材质
|
||||
// @Summary 更新材质
|
||||
// @Description 更新材质信息(名称、描述、公开性)
|
||||
// @Tags texture
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "材质ID"
|
||||
// @Param request body types.UpdateTextureRequest true "更新信息"
|
||||
// @Success 200 {object} model.Response{data=types.TextureInfo} "更新成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
||||
// @Router /api/v1/texture/{id} [put]
|
||||
func (h *TextureHandler) Update(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -205,6 +153,17 @@ func (h *TextureHandler) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Delete 删除材质
|
||||
// @Summary 删除材质
|
||||
// @Description 删除指定ID的材质
|
||||
// @Tags texture
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "材质ID"
|
||||
// @Success 200 {object} model.Response "删除成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
||||
// @Router /api/v1/texture/{id} [delete]
|
||||
func (h *TextureHandler) Delete(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -231,6 +190,16 @@ func (h *TextureHandler) Delete(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ToggleFavorite 切换收藏状态
|
||||
// @Summary 切换收藏状态
|
||||
// @Description 收藏或取消收藏指定材质
|
||||
// @Tags texture
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "材质ID"
|
||||
// @Success 200 {object} model.Response{data=map[string]bool} "操作成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Router /api/v1/texture/{id} [post]
|
||||
func (h *TextureHandler) ToggleFavorite(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -258,6 +227,17 @@ func (h *TextureHandler) ToggleFavorite(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetUserTextures 获取用户上传的材质列表
|
||||
// @Summary 获取我的材质
|
||||
// @Description 获取当前登录用户上传的材质列表
|
||||
// @Tags texture
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||
// @Failure 500 {object} model.ErrorResponse "服务器错误"
|
||||
// @Router /api/v1/texture/my [get]
|
||||
func (h *TextureHandler) GetUserTextures(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -283,6 +263,17 @@ func (h *TextureHandler) GetUserTextures(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetUserFavorites 获取用户收藏的材质列表
|
||||
// @Summary 获取我的收藏
|
||||
// @Description 获取当前登录用户收藏的材质列表
|
||||
// @Tags texture
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||
// @Failure 500 {object} model.ErrorResponse "服务器错误"
|
||||
// @Router /api/v1/texture/favorites [get]
|
||||
func (h *TextureHandler) GetUserFavorites(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -308,6 +299,21 @@ func (h *TextureHandler) GetUserFavorites(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Upload 直接上传材质文件
|
||||
// @Summary 上传材质
|
||||
// @Description 上传图片文件创建新材质
|
||||
// @Tags texture
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param file formData file true "材质文件 (PNG)"
|
||||
// @Param name formData string true "材质名称"
|
||||
// @Param description formData string false "材质描述"
|
||||
// @Param type formData string false "材质类型 (SKIN/CAPE)" default(SKIN)
|
||||
// @Param is_public formData boolean false "是否公开" default(false)
|
||||
// @Param is_slim formData boolean false "是否为纤细模型 (仅SKIN有效)" default(false)
|
||||
// @Success 200 {object} model.Response{data=types.TextureInfo} "上传成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Router /api/v1/texture/upload [post]
|
||||
func (h *TextureHandler) Upload(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/service"
|
||||
"carrotskin/internal/types"
|
||||
|
||||
@@ -24,6 +25,15 @@ func NewUserHandler(c *container.Container) *UserHandler {
|
||||
}
|
||||
|
||||
// GetProfile 获取用户信息
|
||||
// @Summary 获取用户信息
|
||||
// @Description 获取当前登录用户的详细信息
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} model.Response{data=types.UserInfo} "获取成功"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Router /api/v1/user/profile [get]
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -44,6 +54,17 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户信息
|
||||
// @Summary 更新用户信息
|
||||
// @Description 更新用户资料(密码、头像URL),如需上传头像请使用上传接口
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body types.UpdateUserRequest true "更新信息"
|
||||
// @Success 200 {object} model.Response{data=types.UserInfo} "更新成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Router /api/v1/user/profile [put]
|
||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -102,44 +123,88 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
RespondSuccess(c, UserToUserInfo(updatedUser))
|
||||
}
|
||||
|
||||
// GenerateAvatarUploadURL 生成头像上传URL
|
||||
func (h *UserHandler) GenerateAvatarUploadURL(c *gin.Context) {
|
||||
// UploadAvatar 直接上传头像文件
|
||||
// @Summary 上传头像
|
||||
// @Description 上传图片文件作为用户头像
|
||||
// @Tags user
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param file formData file true "头像文件"
|
||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "上传成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Router /api/v1/user/avatar/upload [post]
|
||||
func (h *UserHandler) UploadAvatar(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req types.GenerateAvatarUploadURLRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
RespondBadRequest(c, "请求参数错误", err)
|
||||
// 解析multipart表单
|
||||
if err := c.Request.ParseMultipartForm(10 << 20); err != nil { // 10MB
|
||||
RespondBadRequest(c, "解析表单失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
if h.container.Storage == nil {
|
||||
RespondServerError(c, "存储服务不可用", nil)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.container.UploadService.GenerateAvatarUploadURL(c.Request.Context(), userID, req.FileName)
|
||||
// 获取文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
h.logger.Error("生成头像上传URL失败",
|
||||
RespondBadRequest(c, "获取文件失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
RespondBadRequest(c, "打开文件失败", err)
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
fileData := make([]byte, file.Size)
|
||||
if _, err := src.Read(fileData); err != nil {
|
||||
RespondBadRequest(c, "读取文件失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务上传头像
|
||||
avatarURL, err := h.container.UserService.UploadAvatar(c.Request.Context(), userID, fileData, file.Filename)
|
||||
if err != nil {
|
||||
h.logger.Error("上传头像失败",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("file_name", req.FileName),
|
||||
zap.String("file_name", file.Filename),
|
||||
zap.Error(err),
|
||||
)
|
||||
RespondBadRequest(c, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
RespondSuccess(c, &types.GenerateAvatarUploadURLResponse{
|
||||
PostURL: result.PostURL,
|
||||
FormData: result.FormData,
|
||||
AvatarURL: result.FileURL,
|
||||
ExpiresIn: 900,
|
||||
// 获取更新后的用户信息
|
||||
user, err := h.container.UserService.GetByID(c.Request.Context(), userID)
|
||||
if err != nil || user == nil {
|
||||
RespondNotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
RespondSuccess(c, gin.H{
|
||||
"avatar_url": avatarURL,
|
||||
"user": UserToUserInfo(user),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAvatar 更新头像URL
|
||||
// UpdateAvatar 更新头像URL(保留用于外部URL)
|
||||
// @Summary 更新头像URL
|
||||
// @Description 更新用户头像为外部URL
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param avatar_url query string true "头像URL"
|
||||
// @Success 200 {object} model.Response{data=types.UserInfo} "更新成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Router /api/v1/user/avatar [put]
|
||||
func (h *UserHandler) UpdateAvatar(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -177,6 +242,17 @@ func (h *UserHandler) UpdateAvatar(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ChangeEmail 更换邮箱
|
||||
// @Summary 更换邮箱
|
||||
// @Description 更换用户绑定的邮箱,需要验证码
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body types.ChangeEmailRequest true "更换邮箱请求"
|
||||
// @Success 200 {object} model.Response{data=types.UserInfo} "更换成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Router /api/v1/user/change-email [post]
|
||||
func (h *UserHandler) ChangeEmail(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -215,6 +291,15 @@ func (h *UserHandler) ChangeEmail(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ResetYggdrasilPassword 重置Yggdrasil密码
|
||||
// @Summary 重置Yggdrasil密码
|
||||
// @Description 重置用户的Yggdrasil API认证密码
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} model.Response{data=map[string]string} "重置成功"
|
||||
// @Failure 500 {object} model.ErrorResponse "服务器错误"
|
||||
// @Router /api/v1/user/yggdrasil-password/reset [post]
|
||||
func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) {
|
||||
userID, ok := GetUserIDFromContext(c)
|
||||
if !ok {
|
||||
@@ -231,3 +316,55 @@ func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) {
|
||||
h.logger.Info("Yggdrasil密码重置成功", zap.Int64("userId", userID))
|
||||
RespondSuccess(c, gin.H{"password": newPassword})
|
||||
}
|
||||
|
||||
// GetPublicInfo 获取用户公开信息
|
||||
// @Summary 获取用户公开信息
|
||||
// @Description 根据用户名或用户ID获取用户的公开信息(不包含敏感信息如邮箱)
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string false "用户名"
|
||||
// @Param id query int false "用户ID"
|
||||
// @Success 200 {object} model.Response{data=types.PublicUserInfo} "获取成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Router /api/v1/users/public [get]
|
||||
func (h *UserHandler) GetPublicInfo(c *gin.Context) {
|
||||
username := c.Query("username")
|
||||
idStr := c.Query("id")
|
||||
|
||||
// 至少需要提供一个参数
|
||||
if username == "" && idStr == "" {
|
||||
RespondBadRequest(c, "必须提供用户名或用户ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
var err error
|
||||
|
||||
// 优先使用用户名查询
|
||||
if username != "" {
|
||||
user, err = h.container.UserService.GetByUsername(c.Request.Context(), username)
|
||||
} else {
|
||||
// 使用用户ID查询
|
||||
id := parseIntWithDefault(idStr, 0)
|
||||
if id == 0 {
|
||||
RespondBadRequest(c, "无效的用户ID", nil)
|
||||
return
|
||||
}
|
||||
user, err = h.container.UserService.GetByID(c.Request.Context(), int64(id))
|
||||
}
|
||||
|
||||
if err != nil || user == nil {
|
||||
RespondNotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if user.Status != 1 {
|
||||
RespondNotFound(c, "用户不可用")
|
||||
return
|
||||
}
|
||||
|
||||
RespondSuccess(c, UserToPublicUserInfo(user))
|
||||
}
|
||||
|
||||
@@ -167,6 +167,15 @@ func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler {
|
||||
}
|
||||
|
||||
// Authenticate 用户认证
|
||||
// @Summary Yggdrasil认证
|
||||
// @Description Yggdrasil协议: 用户登录认证
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body AuthenticateRequest true "认证请求"
|
||||
// @Success 200 {object} AuthenticateResponse
|
||||
// @Failure 403 {object} map[string]string "认证失败"
|
||||
// @Router /api/v1/yggdrasil/authserver/authenticate [post]
|
||||
func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
||||
rawData, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
@@ -248,6 +257,15 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ValidToken 验证令牌
|
||||
// @Summary Yggdrasil验证令牌
|
||||
// @Description Yggdrasil协议: 验证AccessToken是否有效
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ValidTokenRequest true "验证请求"
|
||||
// @Success 204 "令牌有效"
|
||||
// @Failure 403 {object} map[string]bool "令牌无效"
|
||||
// @Router /api/v1/yggdrasil/authserver/validate [post]
|
||||
func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
|
||||
var request ValidTokenRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
@@ -266,6 +284,15 @@ func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
// RefreshToken 刷新令牌
|
||||
// @Summary Yggdrasil刷新令牌
|
||||
// @Description Yggdrasil协议: 刷新AccessToken
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshRequest true "刷新请求"
|
||||
// @Success 200 {object} RefreshResponse
|
||||
// @Failure 400 {object} map[string]string "刷新失败"
|
||||
// @Router /api/v1/yggdrasil/authserver/refresh [post]
|
||||
func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
|
||||
var request RefreshRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
@@ -350,6 +377,14 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
// InvalidToken 使令牌失效
|
||||
// @Summary Yggdrasil注销令牌
|
||||
// @Description Yggdrasil协议: 使AccessToken失效
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ValidTokenRequest true "失效请求"
|
||||
// @Success 204 "操作成功"
|
||||
// @Router /api/v1/yggdrasil/authserver/invalidate [post]
|
||||
func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
|
||||
var request ValidTokenRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
@@ -364,6 +399,15 @@ func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
// SignOut 用户登出
|
||||
// @Summary Yggdrasil登出
|
||||
// @Description Yggdrasil协议: 用户登出,使所有令牌失效
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body SignOutRequest true "登出请求"
|
||||
// @Success 204 "操作成功"
|
||||
// @Failure 400 {object} map[string]string "参数错误"
|
||||
// @Router /api/v1/yggdrasil/authserver/signout [post]
|
||||
func (h *YggdrasilHandler) SignOut(c *gin.Context) {
|
||||
var request SignOutRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
@@ -397,6 +441,15 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetProfileByUUID 根据UUID获取档案
|
||||
// @Summary Yggdrasil获取档案
|
||||
// @Description Yggdrasil协议: 根据UUID获取用户档案信息
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param uuid path string true "用户UUID"
|
||||
// @Success 200 {object} map[string]interface{} "档案信息"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid} [get]
|
||||
func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
|
||||
uuid := utils.FormatUUID(c.Param("uuid"))
|
||||
h.logger.Info("获取配置文件请求", zap.String("uuid", uuid))
|
||||
@@ -413,6 +466,16 @@ func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
|
||||
}
|
||||
|
||||
// JoinServer 加入服务器
|
||||
// @Summary Yggdrasil加入服务器
|
||||
// @Description Yggdrasil协议: 客户端加入服务器
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body JoinServerRequest true "加入请求"
|
||||
// @Success 204 "加入成功"
|
||||
// @Failure 400 {object} APIResponse "参数错误"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/join [post]
|
||||
func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
|
||||
var request JoinServerRequest
|
||||
clientIP := c.ClientIP()
|
||||
@@ -449,6 +512,17 @@ func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
// HasJoinedServer 验证玩家是否已加入服务器
|
||||
// @Summary Yggdrasil验证加入
|
||||
// @Description Yggdrasil协议: 服务端验证客户端是否已加入
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string true "用户名"
|
||||
// @Param serverId query string true "服务器ID"
|
||||
// @Param ip query string false "客户端IP"
|
||||
// @Success 200 {object} map[string]interface{} "验证成功,返回档案"
|
||||
// @Failure 204 "验证失败"
|
||||
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/hasJoined [get]
|
||||
func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
||||
clientIP, _ := c.GetQuery("ip")
|
||||
|
||||
@@ -499,6 +573,15 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetProfilesByName 批量获取配置文件
|
||||
// @Summary Yggdrasil批量获取档案
|
||||
// @Description Yggdrasil协议: 根据名称批量获取用户档案
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body []string true "用户名列表"
|
||||
// @Success 200 {array} model.Profile "档案列表"
|
||||
// @Failure 400 {object} APIResponse "参数错误"
|
||||
// @Router /api/v1/yggdrasil/api/profiles/minecraft [post]
|
||||
func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
|
||||
var names []string
|
||||
|
||||
@@ -520,6 +603,14 @@ func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetMetaData 获取Yggdrasil元数据
|
||||
// @Summary Yggdrasil元数据
|
||||
// @Description Yggdrasil协议: 获取服务器元数据
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "元数据"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Router /api/v1/yggdrasil [get]
|
||||
func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
|
||||
meta := gin.H{
|
||||
"implementationName": "CellAuth",
|
||||
@@ -550,6 +641,16 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetPlayerCertificates 获取玩家证书
|
||||
// @Summary Yggdrasil获取证书
|
||||
// @Description Yggdrasil协议: 获取玩家证书
|
||||
// @Tags Yggdrasil
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "Bearer {token}"
|
||||
// @Success 200 {object} map[string]interface{} "证书信息"
|
||||
// @Failure 401 {object} map[string]string "未授权"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Router /api/v1/yggdrasil/minecraftservices/player/certificates [post]
|
||||
func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
|
||||
@@ -51,7 +51,7 @@ func AuthMiddleware(jwtService *auth.JWTService) gin.HandlerFunc {
|
||||
// 将用户信息存储到上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Set("user_role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
})
|
||||
@@ -69,7 +69,7 @@ func OptionalAuthMiddleware(jwtService *auth.JWTService) gin.HandlerFunc {
|
||||
if err == nil {
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Set("user_role", claims.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
109
internal/middleware/casbin.go
Normal file
109
internal/middleware/casbin.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"carrotskin/pkg/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CasbinMiddleware Casbin权限中间件
|
||||
// 需要先经过AuthMiddleware获取用户信息
|
||||
func CasbinMiddleware(casbinService *auth.CasbinService, resource, action string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从上下文获取用户角色(由AuthMiddleware设置)
|
||||
role, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未授权访问",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
roleStr, ok := role.(string)
|
||||
if !ok || roleStr == "" {
|
||||
roleStr = "user" // 默认角色
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if !casbinService.CheckPermission(roleStr, resource, action) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "权限不足",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdmin 要求管理员权限的中间件
|
||||
func RequireAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未授权访问",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
roleStr, ok := role.(string)
|
||||
if !ok || roleStr != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "需要管理员权限",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRole 要求指定角色的中间件
|
||||
func RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未授权访问",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
roleStr, ok := role.(string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "权限不足",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否在允许的角色列表中
|
||||
for _, allowed := range allowedRoles {
|
||||
if roleStr == allowed {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "权限不足",
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
// AuditLog 审计日志模型
|
||||
// @Description 系统操作审计日志记录
|
||||
type AuditLog struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_audit_logs_user_created,priority:1" json:"user_id,omitempty"`
|
||||
@@ -27,6 +28,7 @@ func (AuditLog) TableName() string {
|
||||
}
|
||||
|
||||
// CasbinRule Casbin 权限规则模型
|
||||
// @Description Casbin权限控制规则数据
|
||||
type CasbinRule struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
PType string `gorm:"column:ptype;type:varchar(100);not null;index:idx_casbin_ptype;uniqueIndex:uk_casbin_rule,priority:1" json:"ptype"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// BaseModel 基础模型
|
||||
// @Description 通用基础模型(包含ID和时间戳字段)
|
||||
// 包含 uint 类型的 ID 和标准时间字段,但时间字段不通过 JSON 返回给前端
|
||||
type BaseModel struct {
|
||||
// ID 主键
|
||||
@@ -21,5 +22,3 @@ type BaseModel struct {
|
||||
// DeletedAt 删除时间 (软删除,不返回给前端)
|
||||
DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import "time"
|
||||
|
||||
// Client 客户端实体,用于管理Token版本
|
||||
// @Description Yggdrasil客户端Token管理数据
|
||||
type Client struct {
|
||||
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` // Client UUID
|
||||
ClientToken string `gorm:"column:client_token;type:varchar(64);not null;uniqueIndex" json:"client_token"` // 客户端Token
|
||||
@@ -21,18 +22,3 @@ type Client struct {
|
||||
func (Client) TableName() string {
|
||||
return "clients"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
)
|
||||
|
||||
// Profile Minecraft 档案模型
|
||||
// @Description Minecraft角色档案数据模型
|
||||
type Profile struct {
|
||||
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"`
|
||||
UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1;index:idx_profiles_user_active,priority:1" json:"user_id"`
|
||||
UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1" json:"user_id"`
|
||||
Name string `gorm:"column:name;type:varchar(16);not null;uniqueIndex:idx_profiles_name" json:"name"` // Minecraft 角色名
|
||||
SkinID *int64 `gorm:"column:skin_id;type:bigint;index:idx_profiles_skin_id" json:"skin_id,omitempty"`
|
||||
CapeID *int64 `gorm:"column:cape_id;type:bigint;index:idx_profiles_cape_id" json:"cape_id,omitempty"`
|
||||
RSAPrivateKey string `gorm:"column:rsa_private_key;type:text;not null" json:"-"` // RSA 私钥不返回给前端
|
||||
IsActive bool `gorm:"column:is_active;not null;default:true;index:idx_profiles_user_active,priority:2" json:"is_active"`
|
||||
LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp;index:idx_profiles_last_used,sort:desc" json:"last_used_at,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_profiles_user_created,priority:2,sort:desc" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||
@@ -29,32 +29,37 @@ func (Profile) TableName() string {
|
||||
}
|
||||
|
||||
// ProfileResponse 档案响应(包含完整的皮肤/披风信息)
|
||||
// @Description Minecraft档案完整响应数据
|
||||
type ProfileResponse struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Textures ProfileTexturesData `json:"textures"`
|
||||
IsActive bool `json:"is_active"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ProfileTexturesData Minecraft 材质数据结构
|
||||
// @Description Minecraft档案材质数据
|
||||
type ProfileTexturesData struct {
|
||||
Skin *ProfileTexture `json:"SKIN,omitempty"`
|
||||
Cape *ProfileTexture `json:"CAPE,omitempty"`
|
||||
}
|
||||
|
||||
// ProfileTexture 单个材质信息
|
||||
// @Description 单个材质的详细信息
|
||||
type ProfileTexture struct {
|
||||
URL string `json:"url"`
|
||||
Metadata *ProfileTextureMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ProfileTextureMetadata 材质元数据
|
||||
// @Description 材质的元数据信息
|
||||
type ProfileTextureMetadata struct {
|
||||
Model string `json:"model,omitempty"` // "slim" or "classic"
|
||||
}
|
||||
|
||||
// KeyPair RSA密钥对
|
||||
// @Description 用于Yggdrasil认证的RSA密钥对
|
||||
type KeyPair struct {
|
||||
PrivateKey string `json:"private_key" bson:"private_key"`
|
||||
PublicKey string `json:"public_key" bson:"public_key"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import "os"
|
||||
|
||||
// Response 通用API响应结构
|
||||
// @Description 标准API响应格式
|
||||
type Response struct {
|
||||
Code int `json:"code"` // 业务状态码
|
||||
Message string `json:"message"` // 响应消息
|
||||
@@ -10,6 +11,7 @@ type Response struct {
|
||||
}
|
||||
|
||||
// PaginationResponse 分页响应结构
|
||||
// @Description 分页数据响应格式
|
||||
type PaginationResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -20,6 +22,7 @@ type PaginationResponse struct {
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应
|
||||
// @Description API错误响应格式
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型
|
||||
type ConfigType string
|
||||
|
||||
const (
|
||||
ConfigTypeString ConfigType = "STRING"
|
||||
ConfigTypeInteger ConfigType = "INTEGER"
|
||||
ConfigTypeBoolean ConfigType = "BOOLEAN"
|
||||
ConfigTypeJSON ConfigType = "JSON"
|
||||
)
|
||||
|
||||
// SystemConfig 系统配置模型
|
||||
type SystemConfig struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Key string `gorm:"column:key;type:varchar(100);not null;uniqueIndex" json:"key"`
|
||||
Value string `gorm:"column:value;type:text;not null" json:"value"`
|
||||
Description string `gorm:"column:description;type:varchar(255);not null;default:''" json:"description"`
|
||||
Type ConfigType `gorm:"column:type;type:varchar(50);not null;default:'STRING'" json:"type"` // STRING, INTEGER, BOOLEAN, JSON
|
||||
IsPublic bool `gorm:"column:is_public;not null;default:false;index" json:"is_public"` // 是否可被前端获取
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (SystemConfig) TableName() string {
|
||||
return "system_config"
|
||||
}
|
||||
|
||||
// SystemConfigPublicResponse 公开配置响应
|
||||
type SystemConfigPublicResponse struct {
|
||||
SiteName string `json:"site_name"`
|
||||
SiteDescription string `json:"site_description"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
MaintenanceMode bool `json:"maintenance_mode"`
|
||||
Announcement string `json:"announcement"`
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
// TextureType 材质类型
|
||||
// @Description 材质类型枚举:SKIN(皮肤)或CAPE(披风)
|
||||
type TextureType string
|
||||
|
||||
const (
|
||||
@@ -13,6 +14,7 @@ const (
|
||||
)
|
||||
|
||||
// Texture 材质模型
|
||||
// @Description Minecraft材质数据模型
|
||||
type Texture struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UploaderID int64 `gorm:"column:uploader_id;not null;index:idx_textures_uploader_status,priority:1;index:idx_textures_uploader_created,priority:1" json:"uploader_id"`
|
||||
@@ -40,6 +42,7 @@ func (Texture) TableName() string {
|
||||
}
|
||||
|
||||
// UserTextureFavorite 用户材质收藏
|
||||
// @Description 用户收藏材质关联表
|
||||
type UserTextureFavorite struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_texture,priority:1;index:idx_favorites_user_created,priority:1" json:"user_id"`
|
||||
@@ -57,6 +60,7 @@ func (UserTextureFavorite) TableName() string {
|
||||
}
|
||||
|
||||
// TextureDownloadLog 材质下载记录
|
||||
// @Description 材质下载日志记录
|
||||
type TextureDownloadLog struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
TextureID int64 `gorm:"column:texture_id;not null;index:idx_download_logs_texture_created,priority:1" json:"texture_id"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
// @Description 用户账户数据模型
|
||||
type User struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex:idx_user_username_status,priority:1" json:"username"`
|
||||
@@ -16,7 +17,7 @@ type User struct {
|
||||
Points int `gorm:"column:points;type:integer;not null;default:0;index:idx_user_points,sort:desc" json:"points"`
|
||||
Role string `gorm:"column:role;type:varchar(50);not null;default:'user';index:idx_user_role_status,priority:1" json:"role"`
|
||||
Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_user_username_status,priority:2;index:idx_user_email_status,priority:2;index:idx_user_role_status,priority:2" json:"status"` // 1:正常, 0:禁用, -1:删除
|
||||
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据,存储为PostgreSQL的JSONB类型
|
||||
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty" swaggertype:"string"` // JSON数据,存储为PostgreSQL的JSONB类型
|
||||
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp;index:idx_user_last_login,sort:desc" json:"last_login_at,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_user_created_at,sort:desc" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||
@@ -28,6 +29,7 @@ func (User) TableName() string {
|
||||
}
|
||||
|
||||
// UserPointLog 用户积分变更记录
|
||||
// @Description 用户积分变动日志记录
|
||||
type UserPointLog struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserID int64 `gorm:"column:user_id;not null;index:idx_point_logs_user_created,priority:1" json:"user_id"`
|
||||
@@ -52,6 +54,7 @@ func (UserPointLog) TableName() string {
|
||||
}
|
||||
|
||||
// UserLoginLog 用户登录日志
|
||||
// @Description 用户登录历史记录
|
||||
type UserLoginLog struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserID int64 `gorm:"column:user_id;not null;index:idx_login_logs_user_created,priority:1" json:"user_id"`
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
const passwordChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
// Yggdrasil ygg密码与用户id绑定
|
||||
// @Description Yggdrasil认证密码数据模型
|
||||
type Yggdrasil struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;not null" json:"id"`
|
||||
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 加密后的密码,不返回给前端
|
||||
|
||||
@@ -35,7 +35,6 @@ type ProfileRepository interface {
|
||||
Delete(ctx context.Context, uuid string) error
|
||||
BatchDelete(ctx context.Context, uuids []string) (int64, error) // 批量删除
|
||||
CountByUserID(ctx context.Context, userID int64) (int64, error)
|
||||
SetActive(ctx context.Context, uuid string, userID int64) error
|
||||
UpdateLastUsedAt(ctx context.Context, uuid string) error
|
||||
GetByNames(ctx context.Context, names []string) ([]*model.Profile, error)
|
||||
GetKeyPair(ctx context.Context, profileId string) (*model.KeyPair, error)
|
||||
@@ -67,14 +66,6 @@ type TextureRepository interface {
|
||||
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
|
||||
}
|
||||
|
||||
// SystemConfigRepository 系统配置仓储接口
|
||||
type SystemConfigRepository interface {
|
||||
GetByKey(ctx context.Context, key string) (*model.SystemConfig, error)
|
||||
GetPublic(ctx context.Context) ([]model.SystemConfig, error)
|
||||
GetAll(ctx context.Context) ([]model.SystemConfig, error)
|
||||
Update(ctx context.Context, config *model.SystemConfig) error
|
||||
UpdateValue(ctx context.Context, key, value string) error
|
||||
}
|
||||
|
||||
// YggdrasilRepository Yggdrasil仓储接口
|
||||
type YggdrasilRepository interface {
|
||||
|
||||
@@ -109,20 +109,6 @@ func (r *profileRepository) CountByUserID(ctx context.Context, userID int64) (in
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *profileRepository) SetActive(ctx context.Context, uuid string, userID int64) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Profile{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("is_active", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Model(&model.Profile{}).
|
||||
Where("uuid = ? AND user_id = ?", uuid, userID).
|
||||
Update("is_active", true).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *profileRepository) UpdateLastUsedAt(ctx context.Context, uuid string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Profile{}).
|
||||
Where("uuid = ?", uuid).
|
||||
|
||||
@@ -42,41 +42,6 @@ func TestProfileRepository_QueryConditions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileRepository_SetActiveLogic 测试设置活跃档案的逻辑
|
||||
func TestProfileRepository_SetActiveLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uuid string
|
||||
userID int64
|
||||
otherProfiles int
|
||||
wantAllInactive bool
|
||||
}{
|
||||
{
|
||||
name: "设置一个档案为活跃,其他应该变为非活跃",
|
||||
uuid: "profile-1",
|
||||
userID: 1,
|
||||
otherProfiles: 2,
|
||||
wantAllInactive: true,
|
||||
},
|
||||
{
|
||||
name: "只有一个档案时",
|
||||
uuid: "profile-1",
|
||||
userID: 1,
|
||||
otherProfiles: 0,
|
||||
wantAllInactive: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 验证逻辑:设置一个档案为活跃时,应该先将所有档案设为非活跃
|
||||
if !tt.wantAllInactive {
|
||||
t.Error("Setting active profile should first set all profiles to inactive")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileRepository_CountLogic 测试统计逻辑
|
||||
func TestProfileRepository_CountLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -181,4 +146,3 @@ func TestProfileRepository_FindOneProfileLogic(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestProfileRepository_Basic(t *testing.T) {
|
||||
u := &model.User{Username: "u2", Email: "u2@test.com", Password: "pwd", Status: 1}
|
||||
_ = userRepo.Create(ctx, u)
|
||||
|
||||
p := &model.Profile{UUID: "p-uuid", UserID: u.ID, Name: "hero", IsActive: false}
|
||||
p := &model.Profile{UUID: "p-uuid", UserID: u.ID, Name: "hero"}
|
||||
if err := profileRepo.Create(ctx, p); err != nil {
|
||||
t.Fatalf("create profile err: %v", err)
|
||||
}
|
||||
@@ -98,9 +98,7 @@ func TestProfileRepository_Basic(t *testing.T) {
|
||||
t.Fatalf("CountByUserID mismatch: %d err=%v", count, err)
|
||||
}
|
||||
|
||||
if err := profileRepo.SetActive(ctx, "p-uuid", u.ID); err != nil {
|
||||
t.Fatalf("SetActive err: %v", err)
|
||||
}
|
||||
|
||||
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
|
||||
t.Fatalf("UpdateLastUsedAt err: %v", err)
|
||||
}
|
||||
@@ -208,29 +206,6 @@ func TestTextureRepository_Basic(t *testing.T) {
|
||||
_ = textureRepo.Delete(ctx, tex.ID)
|
||||
}
|
||||
|
||||
func TestSystemConfigRepository_Basic(t *testing.T) {
|
||||
db := testutil.NewTestDB(t)
|
||||
repo := NewSystemConfigRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
cfg := &model.SystemConfig{Key: "site_name", Value: "Carrot", IsPublic: true}
|
||||
if err := repo.Update(ctx, cfg); err != nil {
|
||||
t.Fatalf("Update err: %v", err)
|
||||
}
|
||||
if v, err := repo.GetByKey(ctx, "site_name"); err != nil || v.Value != "Carrot" {
|
||||
t.Fatalf("GetByKey mismatch")
|
||||
}
|
||||
_ = repo.UpdateValue(ctx, "site_name", "Carrot2")
|
||||
if list, _ := repo.GetPublic(ctx); len(list) == 0 {
|
||||
t.Fatalf("GetPublic expected entries")
|
||||
}
|
||||
if all, _ := repo.GetAll(ctx); len(all) == 0 {
|
||||
t.Fatalf("GetAll expected entries")
|
||||
}
|
||||
if v, _ := repo.GetByKey(ctx, "site_name"); v.Value != "Carrot2" {
|
||||
t.Fatalf("UpdateValue not applied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRepository_Basic(t *testing.T) {
|
||||
db := testutil.NewTestDB(t)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// systemConfigRepository SystemConfigRepository的实现
|
||||
type systemConfigRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSystemConfigRepository 创建SystemConfigRepository实例
|
||||
func NewSystemConfigRepository(db *gorm.DB) SystemConfigRepository {
|
||||
return &systemConfigRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *systemConfigRepository) GetByKey(ctx context.Context, key string) (*model.SystemConfig, error) {
|
||||
var config model.SystemConfig
|
||||
err := r.db.WithContext(ctx).Where("key = ?", key).First(&config).Error
|
||||
return handleNotFoundResult(&config, err)
|
||||
}
|
||||
|
||||
func (r *systemConfigRepository) GetPublic(ctx context.Context) ([]model.SystemConfig, error) {
|
||||
var configs []model.SystemConfig
|
||||
err := r.db.WithContext(ctx).Where("is_public = ?", true).Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
func (r *systemConfigRepository) GetAll(ctx context.Context) ([]model.SystemConfig, error) {
|
||||
var configs []model.SystemConfig
|
||||
err := r.db.WithContext(ctx).Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
func (r *systemConfigRepository) Update(ctx context.Context, config *model.SystemConfig) error {
|
||||
return r.db.WithContext(ctx).Save(config).Error
|
||||
}
|
||||
|
||||
func (r *systemConfigRepository) UpdateValue(ctx context.Context, key, value string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSystemConfigRepository_QueryConditions 测试系统配置查询条件逻辑
|
||||
func TestSystemConfigRepository_QueryConditions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
isPublic bool
|
||||
wantValid bool
|
||||
}{
|
||||
{
|
||||
name: "有效的配置键",
|
||||
key: "site_name",
|
||||
isPublic: true,
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "配置键为空",
|
||||
key: "",
|
||||
isPublic: true,
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "公开配置查询",
|
||||
key: "site_name",
|
||||
isPublic: true,
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "私有配置查询",
|
||||
key: "secret_key",
|
||||
isPublic: false,
|
||||
wantValid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isValid := tt.key != ""
|
||||
if isValid != tt.wantValid {
|
||||
t.Errorf("Query condition validation failed: got %v, want %v", isValid, tt.wantValid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemConfigRepository_PublicConfigLogic 测试公开配置逻辑
|
||||
func TestSystemConfigRepository_PublicConfigLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isPublic bool
|
||||
wantInclude bool
|
||||
}{
|
||||
{
|
||||
name: "只获取公开配置",
|
||||
isPublic: true,
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "私有配置不应包含",
|
||||
isPublic: false,
|
||||
wantInclude: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 验证逻辑:GetPublicSystemConfigs应该只返回is_public=true的配置
|
||||
if tt.isPublic != tt.wantInclude {
|
||||
t.Errorf("Public config logic failed: isPublic=%v, wantInclude=%v", tt.isPublic, tt.wantInclude)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemConfigRepository_UpdateValueLogic 测试更新配置值逻辑
|
||||
func TestSystemConfigRepository_UpdateValueLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
wantValid bool
|
||||
}{
|
||||
{
|
||||
name: "有效的键值对",
|
||||
key: "site_name",
|
||||
value: "CarrotSkin",
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "键为空",
|
||||
key: "",
|
||||
value: "CarrotSkin",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "值为空(可能有效)",
|
||||
key: "site_name",
|
||||
value: "",
|
||||
wantValid: true, // 空值也可能是有效的
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isValid := tt.key != ""
|
||||
if isValid != tt.wantValid {
|
||||
t.Errorf("Update value validation failed: got %v, want %v", isValid, tt.wantValid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemConfigRepository_ErrorHandling 测试错误处理逻辑
|
||||
func TestSystemConfigRepository_ErrorHandling(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isNotFound bool
|
||||
wantNilConfig bool
|
||||
}{
|
||||
{
|
||||
name: "记录未找到应该返回nil配置",
|
||||
isNotFound: true,
|
||||
wantNilConfig: true,
|
||||
},
|
||||
{
|
||||
name: "找到记录应该返回配置",
|
||||
isNotFound: false,
|
||||
wantNilConfig: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 验证错误处理逻辑:如果是RecordNotFound,返回nil配置
|
||||
if tt.isNotFound != tt.wantNilConfig {
|
||||
t.Errorf("Error handling logic failed: isNotFound=%v, wantNilConfig=%v", tt.isNotFound, tt.wantNilConfig)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ func (r *textureRepository) FindByID(ctx context.Context, id int64) (*model.Text
|
||||
|
||||
func (r *textureRepository) FindByHash(ctx context.Context, hash string) (*model.Texture, error) {
|
||||
var texture model.Texture
|
||||
err := r.db.WithContext(ctx).Where("hash = ?", hash).First(&texture).Error
|
||||
err := r.db.WithContext(ctx).Preload("Uploader").Where("hash = ?", hash).First(&texture).Error
|
||||
return handleNotFoundResult(&texture, err)
|
||||
}
|
||||
|
||||
func (r *textureRepository) FindByHashAndUploaderID(ctx context.Context, hash string, uploaderID int64) (*model.Texture, error) {
|
||||
var texture model.Texture
|
||||
err := r.db.WithContext(ctx).Where("hash = ? AND uploader_id = ?", hash, uploaderID).First(&texture).Error
|
||||
err := r.db.WithContext(ctx).Preload("Uploader").Where("hash = ? AND uploader_id = ?", hash, uploaderID).First(&texture).Error
|
||||
return handleNotFoundResult(&texture, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,3 +29,9 @@ func (r *yggdrasilRepository) GetPasswordByID(ctx context.Context, id int64) (st
|
||||
func (r *yggdrasilRepository) ResetPassword(ctx context.Context, id int64, password string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Yggdrasil{}).Where("id = ?", id).Update("password", password).Error
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type UserService interface {
|
||||
// 用户查询
|
||||
GetByID(ctx context.Context, id int64) (*model.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*model.User, error)
|
||||
GetByUsername(ctx context.Context, username string) (*model.User, error)
|
||||
|
||||
// 用户更新
|
||||
UpdateInfo(ctx context.Context, user *model.User) error
|
||||
@@ -27,6 +28,9 @@ type UserService interface {
|
||||
ResetPassword(ctx context.Context, email, newPassword string) error
|
||||
ChangeEmail(ctx context.Context, userID int64, newEmail string) error
|
||||
|
||||
// 头像上传
|
||||
UploadAvatar(ctx context.Context, userID int64, fileData []byte, fileName string) (string, error)
|
||||
|
||||
// URL验证
|
||||
ValidateAvatarURL(ctx context.Context, avatarURL string) error
|
||||
|
||||
@@ -45,7 +49,6 @@ type ProfileService interface {
|
||||
Delete(ctx context.Context, uuid string, userID int64) error
|
||||
|
||||
// 档案状态
|
||||
SetActive(ctx context.Context, uuid string, userID int64) error
|
||||
CheckLimit(ctx context.Context, userID int64, maxProfiles int) error
|
||||
|
||||
// 批量查询
|
||||
@@ -56,8 +59,7 @@ type ProfileService interface {
|
||||
// TextureService 材质服务接口
|
||||
type TextureService interface {
|
||||
// 材质CRUD
|
||||
Create(ctx context.Context, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error)
|
||||
UploadTexture(ctx context.Context, uploaderID int64, name, description, textureType string, fileData []byte, fileName string, isPublic, isSlim bool) (*model.Texture, error) // 直接上传材质文件
|
||||
UploadTexture(ctx context.Context, uploaderID int64, name, description, textureType string, fileData []byte, fileName string, isPublic, isSlim bool) (*model.Texture, error)
|
||||
GetByID(ctx context.Context, id int64) (*model.Texture, error)
|
||||
GetByHash(ctx context.Context, hash string) (*model.Texture, error)
|
||||
GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
@@ -99,12 +101,6 @@ type CaptchaService interface {
|
||||
Verify(ctx context.Context, dx int, captchaID string) (bool, error)
|
||||
}
|
||||
|
||||
// UploadService 上传服务接口
|
||||
type UploadService interface {
|
||||
GenerateAvatarUploadURL(ctx context.Context, userID int64, fileName string) (*storage.PresignedPostPolicyResult, error)
|
||||
GenerateTextureUploadURL(ctx context.Context, userID int64, fileName, textureType string) (*storage.PresignedPostPolicyResult, error)
|
||||
}
|
||||
|
||||
// YggdrasilService Yggdrasil服务接口
|
||||
type YggdrasilService interface {
|
||||
// 用户认证
|
||||
@@ -149,7 +145,6 @@ type Services struct {
|
||||
Token TokenService
|
||||
Verification VerificationService
|
||||
Captcha CaptchaService
|
||||
Upload UploadService
|
||||
Yggdrasil YggdrasilService
|
||||
Security SecurityService
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user