Compare commits
3 Commits
7d1c78f965
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| cea22951b9 | |||
|
|
ae3a569f03 | ||
|
|
b23a169925 |
12
.env.example
12
.env.example
@@ -30,6 +30,13 @@ SERVER_READ_TIMEOUT=30s
|
||||
SERVER_WRITE_TIMEOUT=30s
|
||||
SERVER_SWAGGER_ENABLED=true
|
||||
|
||||
# =============================================================================
|
||||
# 应用环境
|
||||
# =============================================================================
|
||||
# 用于 service 层 IsTestEnvironment() 短路验证码/邮件等
|
||||
# 取值:development / test / production
|
||||
ENVIRONMENT=production
|
||||
|
||||
# =============================================================================
|
||||
# 数据库配置
|
||||
# =============================================================================
|
||||
@@ -133,6 +140,11 @@ JWT_EXPIRE_HOURS=168
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
LOG_OUTPUT=logs/app.log
|
||||
# 日志轮转参数(用于 lumberjack)
|
||||
LOG_MAX_SIZE=100 # 单文件最大 MB
|
||||
LOG_MAX_BACKUPS=3 # 保留旧文件数
|
||||
LOG_MAX_AGE=28 # 旧文件最大保留天数
|
||||
LOG_COMPRESS=true # 是否压缩旧文件
|
||||
|
||||
# =============================================================================
|
||||
# 安全配置
|
||||
|
||||
216
README.md
216
README.md
@@ -8,7 +8,7 @@
|
||||
- **邮箱与验证码**:验证码发送频率控制、邮箱绑定与变更
|
||||
- **材质中心**:皮肤/披风上传、搜索、收藏、下载统计、Hash 去重
|
||||
- **角色档案**:Minecraft Profile 管理、RSA 密钥对生成、活跃档案切换
|
||||
- **存储与上传**:RustFS/MinIO 预签名 URL,减轻服务器带宽压力
|
||||
- **存储与上传**:RustFS/MinIO(S3 兼容)直接上传,Hash 分片存储
|
||||
- **任务与日志**:登录日志、操作审计、材质下载记录、定时任务
|
||||
- **权限体系**:Casbin RBAC,支持细粒度路线授权
|
||||
- **配置管理**:100% 依赖环境变量,`SERVER_SWAGGER_ENABLED` 控制 Swagger
|
||||
@@ -18,44 +18,58 @@
|
||||
|
||||
| 类型 | 选型 |
|
||||
| --- | --- |
|
||||
| 语言 / 运行时 | Go 1.24+ |
|
||||
| 语言 / 运行时 | Go 1.25+ |
|
||||
| Web 框架 | Gin |
|
||||
| ORM | GORM (PostgreSQL 驱动) |
|
||||
| 数据库 | PostgreSQL 15+ |
|
||||
| 缓存 / 消息 | Redis 6+ |
|
||||
| 对象存储 | RustFS / MinIO(S3 兼容) |
|
||||
| 权限控制 | Casbin |
|
||||
| 配置 | Viper + `.env` |
|
||||
| 权限控制 | Casbin v3 |
|
||||
| 依赖注入 | uber-go/fx |
|
||||
| 配置 | Viper + godotenv + `.env` |
|
||||
| API 文档 | swaggo / Swagger UI |
|
||||
| 滑动验证码 | wenlng/go-captcha |
|
||||
| 日志 | Uber Zap |
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/server/ # 应用入口(main.go)
|
||||
├── cmd/server/ # 应用入口(main.go:极简 fx 装配)
|
||||
├── 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 定义
|
||||
│ ├── 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.24+
|
||||
- Go 1.25+
|
||||
- PostgreSQL 15+
|
||||
- Redis 6+
|
||||
- RustFS / MinIO(或其他兼容 S3 的对象存储,用于皮肤与头像)
|
||||
- swag(生成 Swagger 文档,见 <https://github.com/swaggo/swag>)
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
@@ -81,25 +95,144 @@ backend/
|
||||
createdb carrotskin
|
||||
# 或 psql -c "CREATE DATABASE carrotskin;"
|
||||
```
|
||||
> 应用启动时会执行 `AutoMigrate`,自动创建 / 更新表结构。
|
||||
> 应用启动时会执行 `AutoMigrate`,自动创建 / 更新表结构,并写入种子数据(默认管理员 + Casbin 规则)。
|
||||
|
||||
5. **启动服务**
|
||||
- **推荐**:`./start.sh`(自动 `swag init`,随后 `go run cmd/server/main.go`)
|
||||
5. **准备对象存储**(创建存储桶 + 配置策略与跨域,**见下方 [🪣 对象存储配置](#-对象存储配置) 章节**)
|
||||
|
||||
6. **启动服务**
|
||||
- **推荐**:`./start.sh`(自动 `swag init`,随后 `go run ./cmd/server`)
|
||||
- **手动启动**:
|
||||
```bash
|
||||
swag init -g cmd/server/main.go -o docs
|
||||
go run cmd/server/main.go
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
6. **访问接口**
|
||||
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 会被判定为 tainted,3D 皮肤将加载失败/显示空白。
|
||||
|
||||
在 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_PORT` | 服务监听端口 | `:8080` |
|
||||
| `SERVER_MODE` | Gin 模式(debug/release) | `debug` |
|
||||
| `SERVER_SWAGGER_ENABLED` | 是否暴露 Swagger UI | `true` |
|
||||
| `DATABASE_HOST` / `DATABASE_PORT` | PostgreSQL 地址 | `localhost` / `5432` |
|
||||
@@ -107,12 +240,16 @@ backend/
|
||||
| `DATABASE_NAME` | 数据库名称 | `carrotskin` |
|
||||
| `REDIS_HOST` / `REDIS_PORT` | Redis 地址 | `localhost` / `6379` |
|
||||
| `REDIS_PASSWORD` | Redis 密码(无可为空) | `` |
|
||||
| `RUSTFS_ENDPOINT` | RustFS/MinIO 访问地址 | `127.0.0.1:9000` |
|
||||
| `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` | 存储桶名称 | `carrotskin-textures` |
|
||||
| `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`。
|
||||
|
||||
@@ -132,17 +269,28 @@ golangci-lint run (若已安装)
|
||||
|
||||
## 🧱 架构说明
|
||||
|
||||
- **分层设计**:Handler -> Service -> Repository -> Model,层次清晰、职责单一。
|
||||
- **依赖管理器**:`pkg/*/manager.go` 使用 `sync.Once` 实现线程安全单例(DB / Redis / Logger / Storage / Email / Auth / Config)。
|
||||
- **分层设计**: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 Server(30s 超时),再关 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 文档。
|
||||
- **配置优先级**:`.env` -> 系统环境变量,所有配置均可通过容器注入。
|
||||
- **自动任务**:`internal/task` 承载后台作业,可按需扩展。
|
||||
|
||||
## 📝 Swagger 说明
|
||||
|
||||
- `start.sh` 会在启动前执行 `swag init -g cmd/server/main.go -o docs`
|
||||
- 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步
|
||||
- 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露
|
||||
- `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 暴露。
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
|
||||
21
cmd/genhash/main.go
Normal file
21
cmd/genhash/main.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"carrotskin/pkg/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: genhash <password>")
|
||||
os.Exit(2)
|
||||
}
|
||||
h, err := auth.HashPassword(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "err:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(h)
|
||||
}
|
||||
@@ -20,6 +20,13 @@ import (
|
||||
// 每个需要资源释放的组件都通过 fx.Lifecycle 注册 OnStop 钩子,
|
||||
// 彻底替代 main.go 里分散的 defer xxx.Close()。
|
||||
var InfraModule = fx.Options(
|
||||
// 顶层 Config 子结构 Provider:让 pkg/* 包的构造函数按值接收细分 Config
|
||||
fx.Provide(func(c *config.Config) config.DatabaseConfig { return c.Database }),
|
||||
fx.Provide(func(c *config.Config) config.RedisConfig { return c.Redis }),
|
||||
fx.Provide(func(c *config.Config) config.RustFSConfig { return c.RustFS }),
|
||||
fx.Provide(func(c *config.Config) config.LogConfig { return c.Log }),
|
||||
fx.Provide(func(c *config.Config) config.EmailConfig { return c.Email }),
|
||||
|
||||
// Logger
|
||||
fx.Provide(logger.New),
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/internal/service"
|
||||
"carrotskin/pkg/auth"
|
||||
@@ -9,8 +12,6 @@ import (
|
||||
"carrotskin/pkg/email"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/storage"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -128,7 +128,6 @@ func NewInternalError(message string, err error) *AppError {
|
||||
// 注意:原此处的 Is/As/Wrap 函数(透传标准库)已删除。
|
||||
// 请直接使用标准库 errors.Is / errors.As / fmt.Errorf。
|
||||
|
||||
|
||||
// YggdrasilErrorResponse Yggdrasil协议标准错误响应格式
|
||||
type YggdrasilErrorResponse struct {
|
||||
Error string `json:"error"` // 错误的简要描述(机器可读)
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/container"
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"carrotskin/internal/container"
|
||||
"net/http"
|
||||
|
||||
"carrotskin/internal/container"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"carrotskin/internal/container"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"carrotskin/internal/container"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/types"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/types"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -2,13 +2,14 @@ package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"carrotskin/pkg/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
)
|
||||
|
||||
// UserRepository 用户仓储接口
|
||||
@@ -11,7 +12,7 @@ type UserRepository interface {
|
||||
FindByID(ctx context.Context, id int64) (*model.User, error)
|
||||
FindByUsername(ctx context.Context, username string) (*model.User, error)
|
||||
FindByEmail(ctx context.Context, email string) (*model.User, error)
|
||||
FindByIDs(ctx context.Context, ids []int64) ([]*model.User, error) // 批量查询
|
||||
FindByIDs(ctx context.Context, ids []int64) ([]*model.User, error) // 批量查询
|
||||
List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) // 分页列表(管理员)
|
||||
Update(ctx context.Context, user *model.User) error
|
||||
UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error
|
||||
@@ -66,10 +67,9 @@ type TextureRepository interface {
|
||||
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
|
||||
ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) // 管理员列表(含 Uploader 预加载)
|
||||
FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) // 管理员查询(无权限过滤)
|
||||
FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) // 管理员查询(无权限过滤)
|
||||
}
|
||||
|
||||
|
||||
// YggdrasilRepository Yggdrasil仓储接口
|
||||
type YggdrasilRepository interface {
|
||||
GetPasswordByID(ctx context.Context, id int64) (string, error)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -34,9 +35,3 @@ func (r *yggdrasilRepository) ResetPassword(ctx context.Context, id int64, passw
|
||||
func (r *yggdrasilRepository) Create(ctx context.Context, yggdrasil *model.Yggdrasil) error {
|
||||
return r.db.WithContext(ctx).Create(yggdrasil).Error
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/pkg/config"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/utils"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"carrotskin/pkg/config"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/utils"
|
||||
|
||||
"github.com/wenlng/go-captcha-assets/resources/imagesv2"
|
||||
"github.com/wenlng/go-captcha-assets/resources/tiles"
|
||||
"github.com/wenlng/go-captcha/v2/slide"
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/storage"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/storage"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/utils"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
@@ -14,6 +9,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/redis"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
@@ -18,6 +15,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/redis"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,11 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/storage"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@@ -16,6 +11,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/storage"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/auth"
|
||||
"carrotskin/pkg/utils"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/auth"
|
||||
"carrotskin/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/auth"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/repository"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -109,4 +110,3 @@ func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Cont
|
||||
func (s *yggdrasilCertificateService) GetPublicKey(ctx context.Context) (string, error) {
|
||||
return s.signatureService.GetPublicKeyFromRedis(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/utils"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/pkg/redis"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/pkg/redis"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
||||
@@ -259,6 +259,7 @@ func setupEnvMappings() {
|
||||
viper.BindEnv("server.read_timeout", "SERVER_READ_TIMEOUT")
|
||||
viper.BindEnv("server.write_timeout", "SERVER_WRITE_TIMEOUT")
|
||||
viper.BindEnv("server.swagger_enabled", "SERVER_SWAGGER_ENABLED")
|
||||
viper.BindEnv("environment", "ENVIRONMENT")
|
||||
|
||||
// 数据库配置
|
||||
viper.BindEnv("database.driver", "DATABASE_DRIVER")
|
||||
@@ -306,6 +307,10 @@ func setupEnvMappings() {
|
||||
viper.BindEnv("log.level", "LOG_LEVEL")
|
||||
viper.BindEnv("log.format", "LOG_FORMAT")
|
||||
viper.BindEnv("log.output", "LOG_OUTPUT")
|
||||
viper.BindEnv("log.max_size", "LOG_MAX_SIZE")
|
||||
viper.BindEnv("log.max_backups", "LOG_MAX_BACKUPS")
|
||||
viper.BindEnv("log.max_age", "LOG_MAX_AGE")
|
||||
viper.BindEnv("log.compress", "LOG_COMPRESS")
|
||||
|
||||
// 邮件配置
|
||||
viper.BindEnv("email.enabled", "EMAIL_ENABLED")
|
||||
@@ -457,10 +462,7 @@ func overrideFromEnv(config *Config) {
|
||||
config.Email.Enabled = emailEnabled == "true" || emailEnabled == "True" || emailEnabled == "TRUE" || emailEnabled == "1"
|
||||
}
|
||||
|
||||
// 处理环境配置
|
||||
if env := os.Getenv("ENVIRONMENT"); env != "" {
|
||||
config.Environment = env
|
||||
}
|
||||
// 处理环境配置(已由 BindEnv("environment", "ENVIRONMENT") + AutomaticEnv 处理)
|
||||
|
||||
// 处理安全配置
|
||||
if allowedOrigins := os.Getenv("SECURITY_ALLOWED_ORIGINS"); allowedOrigins != "" {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"fmt"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ package logger
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
||||
"carrotskin/pkg/config"
|
||||
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
@@ -50,7 +50,7 @@ func New(cfg config.LogConfig) (*zap.Logger, error) {
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
file, err := os.OpenFile(cfg.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -79,7 +79,6 @@ func (s *StorageClient) GetBucket(name string) (string, error) {
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
|
||||
// BuildFileURL 构建文件的公开访问URL
|
||||
func (s *StorageClient) BuildFileURL(bucketName, objectName string) string {
|
||||
return fmt.Sprintf("%s/%s/%s", s.publicURL, bucketName, objectName)
|
||||
|
||||
Reference in New Issue
Block a user