Files
backend/README.md
Wu XiangYu cea22951b9
Some checks failed
Build / build (push) Failing after 39m5s
Build / build-docker (push) Has been skipped
docs: 更新 README,新增对象存储桶策略与 CORS 配置说明
- 新增'对象存储配置'章节:存储桶创建、匿名只读策略、CORS 配置(前端3D渲染必需)
- 明确后端不自动创建存储桶,需手动配置
- 按当前 dev 代码更新:Go 1.25+、Casbin v3、fx 子配置 Provider
- 补充默认管理员账号、RUSTFS_PUBLIC_URL 说明、Docker 部署细节
- 修正启动命令、目录结构、Swagger 产物说明
2026-07-08 17:37:24 +08:00

309 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CarrotSkin Backend
一个功能完善的 Minecraft 皮肤站后端,基于 Go + Gin 构建,覆盖用户认证、材质管理、角色档案、审计日志等核心能力,并提供完整的 Swagger 文档与容器友好的环境变量配置。
## ✨ 功能亮点
- **账号体系**:注册 / 登录 / JWT 鉴权 / Yggdrasil 密码同步 / 用户积分
- **邮箱与验证码**:验证码发送频率控制、邮箱绑定与变更
- **材质中心**:皮肤/披风上传、搜索、收藏、下载统计、Hash 去重
- **角色档案**Minecraft Profile 管理、RSA 密钥对生成、活跃档案切换
- **存储与上传**RustFS/MinIOS3 兼容直接上传Hash 分片存储
- **任务与日志**:登录日志、操作审计、材质下载记录、定时任务
- **权限体系**Casbin RBAC支持细粒度路线授权
- **配置管理**100% 依赖环境变量,`SERVER_SWAGGER_ENABLED` 控制 Swagger
- **可观测性**Zap 结构化日志、统一 API 响应模型
## 🛠 技术栈
| 类型 | 选型 |
| --- | --- |
| 语言 / 运行时 | Go 1.25+ |
| Web 框架 | Gin |
| ORM | GORM (PostgreSQL 驱动) |
| 数据库 | PostgreSQL 15+ |
| 缓存 / 消息 | Redis 6+ |
| 对象存储 | RustFS / MinIOS3 兼容) |
| 权限控制 | Casbin v3 |
| 依赖注入 | uber-go/fx |
| 配置 | Viper + godotenv + `.env` |
| API 文档 | swaggo / Swagger UI |
| 滑动验证码 | wenlng/go-captcha |
| 日志 | Uber Zap |
## 📁 目录结构
```
backend/
├── cmd/server/ # 应用入口main.go极简 fx 装配)
├── internal/
│ ├── app/ # uber-go/fx 装配层(唯一依赖注入入口)
│ │ ├── module.go # 聚合根 Module
│ │ ├── infra_module.go # Logger/DB/Redis/Storage/Email/JWT/Casbin + 子配置 Provider + Lifecycle
│ │ ├── container_module.go # Container 聚合 + 迁移/路由/HTTP/任务生命周期
│ │ ├── repository_module.go # Repository Provider占位Container 模式暂未启用)
│ │ ├── service_module.go # Service Provider占位
│ │ ├── handler_module.go # Handler Provider占位
│ │ ├── server_module.go # HTTP 服务器模块(占位)
│ │ └── task_module.go # 后台任务模块(占位)
│ ├── container/ # 过渡期手动 DI 容器fx 迁移期的聚合层)
│ ├── handler/ # HTTP Handler 与 Swagger 注解
│ ├── service/ # 业务逻辑(接口 + 实现)
│ ├── repository/ # 数据访问(接口 + GORM 实现)
│ ├── model/ # GORM 数据模型与响应结构
│ ├── types/ # 请求/响应 DTO
│ ├── errors/ # 统一错误定义apperrors
│ ├── middleware/ # Gin 中间件
│ └── task/ # 定时任务与后台作业
├── pkg/ # 可复用组件config、database、auth、logger、redis、storage、email、utils
├── configs/casbin/ # Casbin RBAC 模型定义rbac_model.conf
├── start.sh # 启动脚本(自动 swag init
├── docker-compose.yml # 本地容器编排(含 rustfs-init 自动建桶)
├── .env.example # 环境变量示例
└── go.mod # Go Module 定义
```
## ✅ 前置要求
- Go 1.25+
- PostgreSQL 15+
- Redis 6+
- RustFS / MinIO或其他兼容 S3 的对象存储,用于皮肤与头像)
- swag生成 Swagger 文档,见 <https://github.com/swaggo/swag>
## 🚀 快速开始
1. **克隆仓库**
```bash
git clone <repo>
cd backend
```
2. **安装依赖**
```bash
go mod download
```
3. **配置环境变量**
```bash
cp .env.example .env
# 根据实际环境填写数据库、Redis、对象存储、邮件等信息
```
4. **初始化数据库**
```bash
createdb carrotskin
# 或 psql -c "CREATE DATABASE carrotskin;"
```
> 应用启动时会执行 `AutoMigrate`,自动创建 / 更新表结构,并写入种子数据(默认管理员 + Casbin 规则)。
5. **准备对象存储**(创建存储桶 + 配置策略与跨域,**见下方 [🪣 对象存储配置](#-对象存储配置) 章节**
6. **启动服务**
- **推荐**`./start.sh`(自动 `swag init`,随后 `go run ./cmd/server`
- **手动启动**
```bash
swag init -g cmd/server/main.go -o docs
go run ./cmd/server
```
7. **访问接口**
- API Root: `http://localhost:8080`
- Swagger: `http://localhost:8080/swagger/index.html`(需 `SERVER_SWAGGER_ENABLED=true`
- 健康检查: `http://localhost:8080/health`
### 默认管理员
首次启动会自动 seed 一个管理员账号,**首次登录后请立即修改密码**
| 用户名 | 密码 | 邮箱 |
| --- | --- | --- |
| `admin` | `admin123456` | `admin@example.com` |
## 🪣 对象存储配置
后端使用 S3 兼容的对象存储RustFS / MinIO保存皮肤与头像需要两个存储桶
| 桶名(由环境变量指定) | 默认名 | 用途 |
| --- | --- | --- |
| `RUSTFS_BUCKET_TEXTURES` | `carrot-skin-textures` | 皮肤 / 披风文件 |
| `RUSTFS_BUCKET_AVATARS` | `carrot-skin-avatars` | 用户头像文件 |
> ⚠️ **后端不会自动创建存储桶**。请在启动后端前,手动创建这两个桶并配置好访问策略与跨域,否则上传会失败、前端 3D 皮肤将无法渲染。
### 步骤 1创建存储桶
在 RustFS / MinIO 控制台(默认 `http://<host>:9001`)创建上述两个桶,或使用 `mc` 客户端:
```bash
mc alias set myrustfs http://<host>:9000 <access-key> <secret-key>
mc mb myrustfs/carrot-skin-textures --ignore-existing
mc mb myrustfs/carrot-skin-avatars --ignore-existing
```
### 步骤 2配置桶访问策略匿名只读
皮肤和头像需要被浏览器与 Minecraft 启动器**匿名下载**(后端用密钥上传即可)。给每个桶配置如下 S3 Bucket Policy以 `carrot-skin-textures` 为例,`avatars` 桶把 `Resource` 里的桶名换掉即可):
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::carrot-skin-textures/*"
}
]
}
```
等价的 `mc` 命令:
```bash
mc anonymous set download myrustfs/carrot-skin-textures
mc anonymous set download myrustfs/carrot-skin-avatars
```
### 步骤 3配置 CORS跨域**前端 3D 渲染必需**
> 🔴 **这一步极其关键**。普通的 `<img>` 标签加载图片不要求 CORS但前端用 skinview3d / three.js 通过 **WebGL 纹理**渲染 3D 皮肤时,浏览器**强制要求**图片响应带 `Access-Control-Allow-Origin` 头,否则 canvas 会被判定为 tainted3D 皮肤将加载失败/显示空白。
在 RustFS / MinIO 控制台为**两个桶**各配置一条 CORS 规则,允许前端来源以 GET 方法跨域访问。CORS 规则 JSON贴入桶的 CORS 配置编辑器):
```json
[
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 86400
}
]
```
> 生产环境建议把 `AllowedOrigins` 收紧为你的前端实际域名(如 `["https://skins.example.com"]`),而不是 `*`。
等价的 `mc` 命令(将 JSON 存为 `cors.json` 后应用):
```bash
mc cors add myrustfs/carrot-skin-textures < cors.json
mc cors add myrustfs/carrot-skin-avatars < cors.json
```
### 验证存储配置是否生效
匿名 GET 任意一个桶内对象,期望返回 `HTTP 200` 且响应头包含 `access-control-allow-origin`
```bash
curl -I -H "Origin: http://localhost:3000" http://<host>:9000/carrot-skin-textures/<某对象路径>
# 期望看到:
# HTTP/1.1 200 OK
# access-control-allow-origin: *
```
若 `access-control-allow-origin` 缺失,说明 CORS 未生效,前端 3D 皮肤将无法渲染。
### 关于 `RUSTFS_PUBLIC_URL`
`RUSTFS_PUBLIC_URL` 决定后端生成的文件访问 URL 前缀(`<public_url>/<bucket>/<object>`)。
- 留空时:用 `RUSTFS_ENDPOINT` + 协议拼接(适合 endpoint 本身就是浏览器可达地址的场景)
- 生产环境:务必填写**浏览器可访问的公网地址**,例如 `https://rustfs.example.com`,否则后端返回给前端的 URL 浏览器打不开
## 🐳 Docker 部署
`docker-compose.yml` 提供了完整的本地编排,包含 PostgreSQL、Redis、RustFS以及一个一次性的 `rustfs-init` 服务自动创建存储桶并设置匿名下载策略:
```bash
# 启动全部基础设施(含存储)
docker compose --profile storage up -d
# 仅启动应用
docker compose up -d
```
> `rustfs-init` 使用 `mc` 完成建桶与策略配置。CORS 仍需按上文 [步骤 3](#步骤-3配置-cors跨域前端-3d-渲染必需) 手动配置。
## ⚙️ 关键环境变量
| 变量 | 说明 | 示例 |
| --- | --- | --- |
| `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_PUBLIC_URL` | 生成文件 URL 的公开前缀(浏览器可达) | `https://rustfs.example.com` |
| `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` | 对象存储凭据 | `minioadmin` |
| `RUSTFS_BUCKET_TEXTURES` / `RUSTFS_BUCKET_AVATARS` | 存储桶名称 | `carrot-skin-textures` |
| `JWT_SECRET` | JWT 签名密钥 | `change-me` |
| `EMAIL_ENABLED` | 是否开启邮件服务 | `true` |
| `EMAIL_SMTP_HOST` / `EMAIL_SMTP_PORT` | SMTP 配置 | `smtp.example.com` / `587` |
| `ENVIRONMENT` | 应用环境(用于 service 层 `IsTestEnvironment()` 短路) | `production` |
| `SECURITY_ALLOWED_ORIGINS` | API CORS 允许的来源(逗号分隔) | `*` |
| `SECURITY_ALLOWED_DOMAINS` | 头像/材质 URL 允许的域名(逗号分隔) | `localhost,127.0.0.1` |
更多变量请参考 `.env.example` 与 `.env.docker.example`。
## 🧪 常用命令
```bash
# 运行单元测试
go test ./...
# 重新生成 swagger
swag init -g cmd/server/main.go -o docs
# 代码格式化 / 静态检查
gofmt -w .
golangci-lint run (若已安装)
```
## 🧱 架构说明
- **分层设计**Handler → Service → Repository → Model层次清晰、职责单一。
- Handler 不得直连 `*gorm.DB` 或 `*Repository`,必须通过 Service。
- Service 的数据访问通过 Repository 接口(个别需要跨表事务的场景除外)。
- **依赖注入uber-go/fx**
- `internal/app/` 是唯一的依赖装配入口。每个基础设施 / 仓储 / 服务作为 `fx.Provider` 注册。
- **子配置 Provider**`infra_module.go` 从 `*config.Config` 提取 `DatabaseConfig` / `RedisConfig` / `RustFSConfig` / `LogConfig` / `EmailConfig` 等子结构,按值注入到各 `pkg/*` 包的构造函数。
- **生命周期管理**DB / Redis / Storage / Logger / HTTP Server / Task Runner 通过 `fx.Lifecycle` 的 `OnStart` / `OnStop` Hook 统一管理,彻底替代散落在 `main()` 的 `defer Close()` 链。
- **优雅关闭**`fx.New(app.Module(cfg)).Run()` 监听 `SIGINT` / `SIGTERM`,按依赖逆序触发 `OnStop`,先关 HTTP Server30s 超时),再关 Task Runner最后关 DB/Redis。
- **Dev/Test 自动回退**Redis 连接失败且环境为 `development` / `test` / `dev` / `USE_MINIREDIS=true` 时自动启用 `miniredis`。
- **启动时错误检查**:循环依赖、缺失 Provider、Provider 返回 error 全部在 `fx.New()` 阶段立即检测。
- **错误与响应统一**
- 错误统一在 `internal/errors/`(别名 `apperrors`),通过 `errors.Is/As` 匹配。
- 响应统一使用 `model.Response` / `model.PaginationResponse` / `model.NewErrorResponse`。
- Middleware 错误响应也走 `model.NewErrorResponse`,不再使用裸 `gin.H`。
- **配置优先级**`config.Load()` 读取 `.env` → 环境变量 → viper 默认值;`*config.Config` 由 `fx.Supply` 注入。
- **Swagger 注解**:所有 Handler、模型、DTO 均补齐 `@Summary` / `@Description` / `@Success`,可直接生成 OpenAPI 文档。
## 📝 Swagger 说明
- `start.sh` 会在启动前执行 `swag init -g cmd/server/main.go -o docs`,生成 `docs/` 目录(`docs.go` / `swagger.json` / `swagger.yaml`)。
- `docs/` 为生成产物,**不在仓库内跟踪**,首次启动会自动生成。
- 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露。
## 🤝 贡献指南
1. Fork & Clone
2. 创建特性分支:`git checkout -b feature/xxx`
3. 编写代码并补全测试 / Swagger 注释
4. 提交时附上变更说明
## 📄 许可证
该项目未附带开源许可证,默认保留所有权利。若需对外使用,请先与作者确认协议。
---
如需了解业务细节或 API 调用示例,请参考 `docs/swagger.yaml` 或运行服务后访问 Swagger UI。祝编码愉快🍀