Compare commits
2 Commits
a4b6c5011e
...
bdd2be5dc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdd2be5dc5 | ||
|
|
4188ee1555 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,7 +23,8 @@ dist/
|
||||
build/
|
||||
|
||||
# Compiled binaries
|
||||
server
|
||||
/server
|
||||
server.exe
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
|
||||
123
cmd/server/main.go
Normal file
123
cmd/server/main.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "carrotskin/docs" // Swagger文档
|
||||
"carrotskin/internal/handler"
|
||||
"carrotskin/internal/middleware"
|
||||
"carrotskin/pkg/auth"
|
||||
"carrotskin/pkg/config"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/email"
|
||||
"carrotskin/pkg/logger"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/storage"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化配置
|
||||
if err := config.Init(); err != nil {
|
||||
log.Fatalf("配置加载失败: %v", err)
|
||||
}
|
||||
cfg := config.MustGetConfig()
|
||||
|
||||
// 初始化日志
|
||||
if err := logger.Init(cfg.Log); err != nil {
|
||||
log.Fatalf("日志初始化失败: %v", err)
|
||||
}
|
||||
loggerInstance := logger.MustGetLogger()
|
||||
defer loggerInstance.Sync()
|
||||
|
||||
// 初始化数据库
|
||||
if err := database.Init(cfg.Database, loggerInstance); err != nil {
|
||||
loggerInstance.Fatal("数据库初始化失败", zap.Error(err))
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// 执行数据库迁移
|
||||
if err := database.AutoMigrate(loggerInstance); err != nil {
|
||||
loggerInstance.Fatal("数据库迁移失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化JWT服务
|
||||
if err := auth.Init(cfg.JWT); err != nil {
|
||||
loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化Redis
|
||||
if err := redis.Init(cfg.Redis, loggerInstance); err != nil {
|
||||
loggerInstance.Fatal("Redis连接失败", zap.Error(err))
|
||||
}
|
||||
defer redis.MustGetClient().Close()
|
||||
|
||||
// 初始化对象存储 (RustFS - S3兼容)
|
||||
// 如果对象存储未配置或连接失败,记录警告但不退出(某些功能可能不可用)
|
||||
if err := storage.Init(cfg.RustFS); err != nil {
|
||||
loggerInstance.Warn("对象存储连接失败,某些功能可能不可用", zap.Error(err))
|
||||
} else {
|
||||
loggerInstance.Info("对象存储连接成功")
|
||||
}
|
||||
|
||||
// 初始化邮件服务
|
||||
if err := email.Init(cfg.Email, loggerInstance); err != nil {
|
||||
loggerInstance.Fatal("邮件服务初始化失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 设置Gin模式
|
||||
if cfg.Server.Mode == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// 创建路由
|
||||
router := gin.New()
|
||||
|
||||
// 添加中间件
|
||||
router.Use(middleware.Logger(loggerInstance))
|
||||
router.Use(middleware.Recovery(loggerInstance))
|
||||
router.Use(middleware.CORS())
|
||||
|
||||
// 注册路由
|
||||
handler.RegisterRoutes(router)
|
||||
|
||||
// 创建HTTP服务器
|
||||
srv := &http.Server{
|
||||
Addr: cfg.Server.Port,
|
||||
Handler: router,
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
go func() {
|
||||
loggerInstance.Info("服务器启动", zap.String("port", cfg.Server.Port))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
loggerInstance.Fatal("服务器启动失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号优雅关闭
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
loggerInstance.Info("正在关闭服务器...")
|
||||
|
||||
// 设置关闭超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
loggerInstance.Fatal("服务器强制关闭", zap.Error(err))
|
||||
}
|
||||
|
||||
loggerInstance.Info("服务器已关闭")
|
||||
}
|
||||
17
go.mod
17
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/minio/minio-go/v7 v7.0.66
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
github.com/spf13/viper v1.21.0
|
||||
@@ -17,13 +18,19 @@ require (
|
||||
github.com/wenlng/go-captcha-assets v1.0.7
|
||||
github.com/wenlng/go-captcha/v2 v2.0.4
|
||||
go.uber.org/zap v1.26.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/image v0.16.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -47,10 +54,10 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.15.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.4.3
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
|
||||
46
go.sum
46
go.sum
@@ -1,3 +1,5 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
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=
|
||||
@@ -54,12 +56,19 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
|
||||
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
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=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -67,14 +76,16 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.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/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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
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/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=
|
||||
@@ -102,12 +113,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
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/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
|
||||
@@ -254,8 +271,17 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
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.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -37,6 +37,9 @@ func RegisterRoutes(router *gin.Engine) {
|
||||
|
||||
// 更换邮箱
|
||||
userGroup.POST("/change-email", ChangeEmail)
|
||||
|
||||
// Yggdrasil密码相关
|
||||
userGroup.POST("/yggdrasil-password/reset", ResetYggdrasilPassword) // 重置Yggdrasil密码并返回新密码
|
||||
}
|
||||
|
||||
// 材质路由
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"carrotskin/internal/service"
|
||||
"carrotskin/internal/types"
|
||||
"carrotskin/pkg/config"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/logger"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/storage"
|
||||
@@ -413,3 +414,49 @@ func ChangeEmail(c *gin.Context) {
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}))
|
||||
}
|
||||
|
||||
// ResetYggdrasilPassword 重置Yggdrasil密码
|
||||
// @Summary 重置Yggdrasil密码
|
||||
// @Description 重置当前用户的Yggdrasil密码并返回新密码
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} model.Response "重置成功"
|
||||
// @Failure 401 {object} model.ErrorResponse "未授权"
|
||||
// @Failure 500 {object} model.ErrorResponse "服务器错误"
|
||||
// @Router /api/v1/user/yggdrasil-password/reset [post]
|
||||
func ResetYggdrasilPassword(c *gin.Context) {
|
||||
loggerInstance := logger.MustGetLogger()
|
||||
db := database.MustGetDB()
|
||||
|
||||
// 从上下文获取用户ID
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
|
||||
model.CodeUnauthorized,
|
||||
"未授权",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
userId := userID.(int64)
|
||||
|
||||
// 重置Yggdrasil密码
|
||||
newPassword, err := service.ResetYggdrasilPassword(db, userId)
|
||||
if err != nil {
|
||||
loggerInstance.Error("[ERROR] 重置Yggdrasil密码失败", zap.Error(err), zap.Int64("userId", userId))
|
||||
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(
|
||||
model.CodeServerError,
|
||||
"重置Yggdrasil密码失败",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
loggerInstance.Info("[INFO] Yggdrasil密码重置成功", zap.Int64("userId", userId))
|
||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||
"password": newPassword,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -234,10 +234,8 @@ func Authenticate(c *gin.Context) {
|
||||
response.SelectedProfile = service.SerializeProfile(db, loggerInstance, redisClient, *selectedProfile)
|
||||
}
|
||||
if request.RequestUser {
|
||||
response.User = map[string]interface{}{
|
||||
"id": userId,
|
||||
"properties": user.Properties,
|
||||
}
|
||||
// 使用 SerializeUser 来正确处理 Properties 字段
|
||||
response.User = service.SerializeUser(loggerInstance, user, UUID)
|
||||
}
|
||||
|
||||
// 返回认证响应
|
||||
|
||||
@@ -2,22 +2,24 @@ package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"`
|
||||
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端
|
||||
Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex" json:"email"`
|
||||
Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" json:"avatar"`
|
||||
Points int `gorm:"column:points;type:integer;not null;default:0" json:"points"`
|
||||
Role string `gorm:"column:role;type:varchar(50);not null;default:'user'" json:"role"`
|
||||
Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"` // 1:正常, 0:禁用, -1:删除
|
||||
Properties string `gorm:"column:properties;type:jsonb" json:"properties"` // JSON字符串,存储为PostgreSQL的JSONB类型
|
||||
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp" json:"last_login_at,omitempty"`
|
||||
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"`
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"`
|
||||
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端
|
||||
Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex" json:"email"`
|
||||
Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" json:"avatar"`
|
||||
Points int `gorm:"column:points;type:integer;not null;default:0" json:"points"`
|
||||
Role string `gorm:"column:role;type:varchar(50);not null;default:'user'" json:"role"`
|
||||
Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"` // 1:正常, 0:禁用, -1:删除
|
||||
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据,存储为PostgreSQL的JSONB类型
|
||||
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp" json:"last_login_at,omitempty"`
|
||||
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 指定表名
|
||||
|
||||
@@ -14,3 +14,9 @@ func GetYggdrasilPasswordById(Id int64) (string, error) {
|
||||
}
|
||||
return yggdrasil.Password, nil
|
||||
}
|
||||
|
||||
// ResetYggdrasilPassword 重置Yggdrasil密码
|
||||
func ResetYggdrasilPassword(userId int64, newPassword string) error {
|
||||
db := database.MustGetDB()
|
||||
return db.Model(&model.Yggdrasil{}).Where("id = ?", userId).Update("password", newPassword).Error
|
||||
}
|
||||
@@ -129,13 +129,19 @@ func GenerateCaptchaData(ctx context.Context, redisClient *redis.Client) (string
|
||||
redisDataJSON,
|
||||
expireTime,
|
||||
); err != nil {
|
||||
return "", "", "", 0, fmt.Errorf("存储验证码到Redis失败: %w", err)
|
||||
return "", "", "", 0, fmt.Errorf("存储验证码到redis失败: %w", err)
|
||||
}
|
||||
return mBase64, tBase64, captchaID, y - 10, nil
|
||||
}
|
||||
|
||||
// VerifyCaptchaData 验证用户验证码
|
||||
func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, id string) (bool, error) {
|
||||
// 测试环境下直接通过验证
|
||||
cfg, err := config.GetConfig()
|
||||
if err == nil && cfg.IsTestEnvironment() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
redisKey := redisKeyPrefix + id
|
||||
|
||||
// 从Redis获取验证信息,使用注入的客户端
|
||||
@@ -144,11 +150,11 @@ func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, i
|
||||
if redisClient.Nil(err) { // 使用封装客户端的Nil错误
|
||||
return false, errors.New("验证码已过期或无效")
|
||||
}
|
||||
return false, fmt.Errorf("Redis查询失败: %w", err)
|
||||
return false, fmt.Errorf("redis查询失败: %w", err)
|
||||
}
|
||||
var redisData RedisData
|
||||
if err := json.Unmarshal([]byte(dataJSON), &redisData); err != nil {
|
||||
return false, fmt.Errorf("解析Redis数据失败: %w", err)
|
||||
return false, fmt.Errorf("解析redis数据失败: %w", err)
|
||||
}
|
||||
tx := redisData.Tx
|
||||
ty := redisData.Ty
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/redis"
|
||||
"encoding/base64"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -90,8 +91,23 @@ func SerializeUser(logger *zap.Logger, u *model.User, UUID string) map[string]in
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"id": UUID,
|
||||
"properties": u.Properties,
|
||||
"id": UUID,
|
||||
}
|
||||
|
||||
// 正确处理 *datatypes.JSON 指针类型
|
||||
// 如果 Properties 为 nil,则设置为 nil;否则解引用并解析为 JSON 值
|
||||
if u.Properties == nil {
|
||||
data["properties"] = nil
|
||||
} else {
|
||||
// datatypes.JSON 是 []byte 类型,需要解析为实际的 JSON 值
|
||||
var propertiesValue interface{}
|
||||
if err := json.Unmarshal(*u.Properties, &propertiesValue); err != nil {
|
||||
logger.Warn("[WARN] 解析用户Properties失败,使用空值", zap.Error(err))
|
||||
data["properties"] = nil
|
||||
} else {
|
||||
data["properties"] = propertiesValue
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ func TestSerializeUser_NilUser(t *testing.T) {
|
||||
func TestSerializeUser_ActualCall(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
user := &model.User{
|
||||
ID: 1,
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Properties: "{}",
|
||||
ID: 1,
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
// Properties 使用 datatypes.JSON,测试中可以为空
|
||||
}
|
||||
|
||||
result := SerializeUser(logger, user, "test-uuid-123")
|
||||
|
||||
@@ -50,6 +50,7 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
|
||||
Role: "user",
|
||||
Status: 1,
|
||||
Points: 0, // 初始积分可以从配置读取
|
||||
// Properties 字段使用 datatypes.JSON,默认为 nil,数据库会存储 NULL
|
||||
}
|
||||
|
||||
if err := repository.CreateUser(user); err != nil {
|
||||
|
||||
@@ -7,18 +7,19 @@ import (
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"carrotskin/pkg/config"
|
||||
"carrotskin/pkg/email"
|
||||
"carrotskin/pkg/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
// 验证码类型
|
||||
VerificationTypeRegister = "register"
|
||||
VerificationTypeRegister = "register"
|
||||
VerificationTypeResetPassword = "reset_password"
|
||||
VerificationTypeChangeEmail = "change_email"
|
||||
|
||||
|
||||
// 验证码配置
|
||||
CodeLength = 6 // 验证码长度
|
||||
CodeLength = 6 // 验证码长度
|
||||
CodeExpiration = 10 * time.Minute // 验证码有效期
|
||||
CodeRateLimit = 1 * time.Minute // 发送频率限制
|
||||
)
|
||||
@@ -39,6 +40,12 @@ func GenerateVerificationCode() (string, error) {
|
||||
|
||||
// SendVerificationCode 发送验证码
|
||||
func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailService *email.Service, email, codeType string) error {
|
||||
// 测试环境下直接跳过,不存储也不发送
|
||||
cfg, err := config.GetConfig()
|
||||
if err == nil && cfg.IsTestEnvironment() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查发送频率限制
|
||||
rateLimitKey := fmt.Sprintf("verification:rate_limit:%s:%s", codeType, email)
|
||||
exists, err := redisClient.Exists(ctx, rateLimitKey)
|
||||
@@ -78,8 +85,14 @@ func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailS
|
||||
|
||||
// VerifyCode 验证验证码
|
||||
func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, codeType string) error {
|
||||
// 测试环境下直接通过验证
|
||||
cfg, err := config.GetConfig()
|
||||
if err == nil && cfg.IsTestEnvironment() {
|
||||
return nil
|
||||
}
|
||||
|
||||
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
|
||||
|
||||
|
||||
// 从Redis获取验证码
|
||||
storedCode, err := redisClient.Get(ctx, codeKey)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -78,6 +79,33 @@ func GetPasswordByUserId(db *gorm.DB, userId int64) (string, error) {
|
||||
return passwordStore, nil
|
||||
}
|
||||
|
||||
// ResetYggdrasilPassword 重置并返回新的Yggdrasil密码
|
||||
func ResetYggdrasilPassword(db *gorm.DB, userId int64) (string, error) {
|
||||
// 生成新的16位随机密码
|
||||
newPassword := model.GenerateRandomPassword(16)
|
||||
|
||||
// 检查Yggdrasil记录是否存在
|
||||
_, err := repository.GetYggdrasilPasswordById(userId)
|
||||
if err != nil {
|
||||
// 如果不存在,创建新记录
|
||||
yggdrasil := model.Yggdrasil{
|
||||
ID: userId,
|
||||
Password: newPassword,
|
||||
}
|
||||
if err := db.Create(&yggdrasil).Error; err != nil {
|
||||
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
|
||||
}
|
||||
return newPassword, nil
|
||||
}
|
||||
|
||||
// 如果存在,更新密码
|
||||
if err := repository.ResetYggdrasilPassword(userId, newPassword); err != nil {
|
||||
return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
|
||||
}
|
||||
|
||||
return newPassword, nil
|
||||
}
|
||||
|
||||
// JoinServer 记录玩家加入服务器的会话信息
|
||||
func JoinServer(db *gorm.DB, logger *zap.Logger, redisClient *redis.Client, serverId, accessToken, selectedProfile, ip string) error {
|
||||
// 输入验证
|
||||
|
||||
@@ -12,15 +12,16 @@ import (
|
||||
|
||||
// Config 应用配置结构体
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
RustFS RustFSConfig `mapstructure:"rustfs"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Casbin CasbinConfig `mapstructure:"casbin"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
Upload UploadConfig `mapstructure:"upload"`
|
||||
Email EmailConfig `mapstructure:"email"`
|
||||
Environment string `mapstructure:"environment"`
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
RustFS RustFSConfig `mapstructure:"rustfs"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Casbin CasbinConfig `mapstructure:"casbin"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
Upload UploadConfig `mapstructure:"upload"`
|
||||
Email EmailConfig `mapstructure:"email"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
@@ -78,21 +79,21 @@ type CasbinConfig struct {
|
||||
|
||||
// LogConfig 日志配置
|
||||
type LogConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// UploadConfig 文件上传配置
|
||||
type UploadConfig struct {
|
||||
MaxSize int64 `mapstructure:"max_size"`
|
||||
AllowedTypes []string `mapstructure:"allowed_types"`
|
||||
TextureMaxSize int64 `mapstructure:"texture_max_size"`
|
||||
AvatarMaxSize int64 `mapstructure:"avatar_max_size"`
|
||||
MaxSize int64 `mapstructure:"max_size"`
|
||||
AllowedTypes []string `mapstructure:"allowed_types"`
|
||||
TextureMaxSize int64 `mapstructure:"texture_max_size"`
|
||||
AvatarMaxSize int64 `mapstructure:"avatar_max_size"`
|
||||
}
|
||||
|
||||
// EmailConfig 邮件配置
|
||||
@@ -109,14 +110,14 @@ type EmailConfig struct {
|
||||
func Load() (*Config, error) {
|
||||
// 加载.env文件(如果存在)
|
||||
_ = godotenv.Load(".env")
|
||||
|
||||
|
||||
// 设置默认值
|
||||
setDefaults()
|
||||
|
||||
|
||||
// 设置环境变量前缀
|
||||
viper.SetEnvPrefix("CARROTSKIN")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
|
||||
// 手动设置环境变量映射
|
||||
setupEnvMappings()
|
||||
|
||||
@@ -125,7 +126,7 @@ func Load() (*Config, error) {
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 从环境变量中覆盖配置
|
||||
overrideFromEnv(&config)
|
||||
|
||||
@@ -139,7 +140,7 @@ func setDefaults() {
|
||||
viper.SetDefault("server.mode", "debug")
|
||||
viper.SetDefault("server.read_timeout", "30s")
|
||||
viper.SetDefault("server.write_timeout", "30s")
|
||||
|
||||
|
||||
// 数据库默认配置
|
||||
viper.SetDefault("database.driver", "postgres")
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
@@ -149,24 +150,24 @@ func setDefaults() {
|
||||
viper.SetDefault("database.max_idle_conns", 10)
|
||||
viper.SetDefault("database.max_open_conns", 100)
|
||||
viper.SetDefault("database.conn_max_lifetime", "1h")
|
||||
|
||||
|
||||
// Redis默认配置
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", 6379)
|
||||
viper.SetDefault("redis.database", 0)
|
||||
viper.SetDefault("redis.pool_size", 10)
|
||||
|
||||
|
||||
// RustFS默认配置
|
||||
viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000")
|
||||
viper.SetDefault("rustfs.use_ssl", false)
|
||||
|
||||
|
||||
// JWT默认配置
|
||||
viper.SetDefault("jwt.expire_hours", 168)
|
||||
|
||||
|
||||
// Casbin默认配置
|
||||
viper.SetDefault("casbin.model_path", "configs/casbin/rbac_model.conf")
|
||||
viper.SetDefault("casbin.policy_adapter", "gorm")
|
||||
|
||||
|
||||
// 日志默认配置
|
||||
viper.SetDefault("log.level", "info")
|
||||
viper.SetDefault("log.format", "json")
|
||||
@@ -175,13 +176,13 @@ func setDefaults() {
|
||||
viper.SetDefault("log.max_backups", 3)
|
||||
viper.SetDefault("log.max_age", 28)
|
||||
viper.SetDefault("log.compress", true)
|
||||
|
||||
|
||||
// 文件上传默认配置
|
||||
viper.SetDefault("upload.max_size", 10485760)
|
||||
viper.SetDefault("upload.texture_max_size", 2097152)
|
||||
viper.SetDefault("upload.avatar_max_size", 1048576)
|
||||
viper.SetDefault("upload.allowed_types", []string{"image/png", "image/jpeg"})
|
||||
|
||||
|
||||
// 邮件默认配置
|
||||
viper.SetDefault("email.enabled", false)
|
||||
viper.SetDefault("email.smtp_port", 587)
|
||||
@@ -194,7 +195,7 @@ func setupEnvMappings() {
|
||||
viper.BindEnv("server.mode", "SERVER_MODE")
|
||||
viper.BindEnv("server.read_timeout", "SERVER_READ_TIMEOUT")
|
||||
viper.BindEnv("server.write_timeout", "SERVER_WRITE_TIMEOUT")
|
||||
|
||||
|
||||
// 数据库配置
|
||||
viper.BindEnv("database.driver", "DATABASE_DRIVER")
|
||||
viper.BindEnv("database.host", "DATABASE_HOST")
|
||||
@@ -204,28 +205,28 @@ func setupEnvMappings() {
|
||||
viper.BindEnv("database.database", "DATABASE_NAME")
|
||||
viper.BindEnv("database.ssl_mode", "DATABASE_SSL_MODE")
|
||||
viper.BindEnv("database.timezone", "DATABASE_TIMEZONE")
|
||||
|
||||
|
||||
// Redis配置
|
||||
viper.BindEnv("redis.host", "REDIS_HOST")
|
||||
viper.BindEnv("redis.port", "REDIS_PORT")
|
||||
viper.BindEnv("redis.password", "REDIS_PASSWORD")
|
||||
viper.BindEnv("redis.database", "REDIS_DATABASE")
|
||||
|
||||
|
||||
// RustFS配置
|
||||
viper.BindEnv("rustfs.endpoint", "RUSTFS_ENDPOINT")
|
||||
viper.BindEnv("rustfs.access_key", "RUSTFS_ACCESS_KEY")
|
||||
viper.BindEnv("rustfs.secret_key", "RUSTFS_SECRET_KEY")
|
||||
viper.BindEnv("rustfs.use_ssl", "RUSTFS_USE_SSL")
|
||||
|
||||
|
||||
// JWT配置
|
||||
viper.BindEnv("jwt.secret", "JWT_SECRET")
|
||||
viper.BindEnv("jwt.expire_hours", "JWT_EXPIRE_HOURS")
|
||||
|
||||
|
||||
// 日志配置
|
||||
viper.BindEnv("log.level", "LOG_LEVEL")
|
||||
viper.BindEnv("log.format", "LOG_FORMAT")
|
||||
viper.BindEnv("log.output", "LOG_OUTPUT")
|
||||
|
||||
|
||||
// 邮件配置
|
||||
viper.BindEnv("email.enabled", "EMAIL_ENABLED")
|
||||
viper.BindEnv("email.smtp_host", "EMAIL_SMTP_HOST")
|
||||
@@ -244,61 +245,71 @@ func overrideFromEnv(config *Config) {
|
||||
}
|
||||
config.RustFS.Buckets["textures"] = texturesBucket
|
||||
}
|
||||
|
||||
|
||||
if avatarsBucket := os.Getenv("RUSTFS_BUCKET_AVATARS"); avatarsBucket != "" {
|
||||
if config.RustFS.Buckets == nil {
|
||||
config.RustFS.Buckets = make(map[string]string)
|
||||
}
|
||||
config.RustFS.Buckets["avatars"] = avatarsBucket
|
||||
}
|
||||
|
||||
|
||||
// 处理数据库连接池配置
|
||||
if maxIdleConns := os.Getenv("DATABASE_MAX_IDLE_CONNS"); maxIdleConns != "" {
|
||||
if val, err := strconv.Atoi(maxIdleConns); err == nil {
|
||||
config.Database.MaxIdleConns = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if maxOpenConns := os.Getenv("DATABASE_MAX_OPEN_CONNS"); maxOpenConns != "" {
|
||||
if val, err := strconv.Atoi(maxOpenConns); err == nil {
|
||||
config.Database.MaxOpenConns = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if connMaxLifetime := os.Getenv("DATABASE_CONN_MAX_LIFETIME"); connMaxLifetime != "" {
|
||||
if val, err := time.ParseDuration(connMaxLifetime); err == nil {
|
||||
config.Database.ConnMaxLifetime = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 处理Redis池大小
|
||||
if poolSize := os.Getenv("REDIS_POOL_SIZE"); poolSize != "" {
|
||||
if val, err := strconv.Atoi(poolSize); err == nil {
|
||||
config.Redis.PoolSize = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 处理文件上传配置
|
||||
if maxSize := os.Getenv("UPLOAD_MAX_SIZE"); maxSize != "" {
|
||||
if val, err := strconv.ParseInt(maxSize, 10, 64); err == nil {
|
||||
config.Upload.MaxSize = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if textureMaxSize := os.Getenv("UPLOAD_TEXTURE_MAX_SIZE"); textureMaxSize != "" {
|
||||
if val, err := strconv.ParseInt(textureMaxSize, 10, 64); err == nil {
|
||||
config.Upload.TextureMaxSize = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if avatarMaxSize := os.Getenv("UPLOAD_AVATAR_MAX_SIZE"); avatarMaxSize != "" {
|
||||
if val, err := strconv.ParseInt(avatarMaxSize, 10, 64); err == nil {
|
||||
config.Upload.AvatarMaxSize = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 处理邮件配置
|
||||
if emailEnabled := os.Getenv("EMAIL_ENABLED"); emailEnabled != "" {
|
||||
config.Email.Enabled = emailEnabled == "true" || emailEnabled == "True" || emailEnabled == "TRUE" || emailEnabled == "1"
|
||||
}
|
||||
|
||||
// 处理环境配置
|
||||
if env := os.Getenv("ENVIRONMENT"); env != "" {
|
||||
config.Environment = env
|
||||
}
|
||||
}
|
||||
|
||||
// IsTestEnvironment 判断是否为测试环境
|
||||
func (c *Config) IsTestEnvironment() bool {
|
||||
return c.Environment == "test"
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ func AutoMigrate(logger *zap.Logger) error {
|
||||
logger.Info("开始执行数据库迁移...")
|
||||
|
||||
// 迁移所有表 - 注意顺序:先创建被引用的表,再创建引用表
|
||||
err = db.AutoMigrate(
|
||||
// 使用分批迁移,避免某些表的问题影响其他表
|
||||
tables := []interface{}{
|
||||
// 用户相关表(先创建,因为其他表可能引用它)
|
||||
&model.User{},
|
||||
&model.UserPointLog{},
|
||||
@@ -87,11 +88,30 @@ func AutoMigrate(logger *zap.Logger) error {
|
||||
|
||||
// Casbin权限规则表
|
||||
&model.CasbinRule{},
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("数据库迁移失败", zap.Error(err))
|
||||
return fmt.Errorf("数据库迁移失败: %w", err)
|
||||
// 逐个迁移表,以便更好地定位问题
|
||||
for _, table := range tables {
|
||||
tableName := fmt.Sprintf("%T", table)
|
||||
logger.Info("正在迁移表", zap.String("table", tableName))
|
||||
if err := db.AutoMigrate(table); err != nil {
|
||||
logger.Error("数据库迁移失败", zap.Error(err), zap.String("table", tableName))
|
||||
// 如果是 User 表且错误是 insufficient arguments,可能是 Properties 字段问题
|
||||
if tableName == "*model.User" {
|
||||
logger.Warn("User 表迁移失败,可能是 Properties 字段问题,尝试修复...")
|
||||
// 尝试手动添加 properties 字段(如果不存在)
|
||||
if err := db.Exec("ALTER TABLE \"user\" ADD COLUMN IF NOT EXISTS properties jsonb").Error; err != nil {
|
||||
logger.Error("添加 properties 字段失败", zap.Error(err))
|
||||
}
|
||||
// 再次尝试迁移
|
||||
if err := db.AutoMigrate(table); err != nil {
|
||||
return fmt.Errorf("数据库迁移失败 (表: %T): %w", table, err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("数据库迁移失败 (表: %T): %w", table, err)
|
||||
}
|
||||
}
|
||||
logger.Info("表迁移成功", zap.String("table", tableName))
|
||||
}
|
||||
|
||||
logger.Info("数据库迁移完成")
|
||||
|
||||
293
scripts/generate_test_account.py
Normal file
293
scripts/generate_test_account.py
Normal file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CarrotSkin 测试账户生成器
|
||||
此脚本创建新用户、角色,并输出所有测试信息
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("错误: 需要安装 requests 库")
|
||||
print("请运行: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
# 颜色输出支持(Windows 和 Unix 都支持)
|
||||
try:
|
||||
from colorama import init, Fore, Style
|
||||
init(autoreset=True)
|
||||
HAS_COLORAMA = True
|
||||
# colorama 没有 GRAY,使用 WHITE 代替
|
||||
Fore.GRAY = Fore.WHITE
|
||||
except ImportError:
|
||||
# 如果没有 colorama,使用空字符串
|
||||
class Fore:
|
||||
CYAN = YELLOW = GREEN = RED = GRAY = WHITE = RESET = ""
|
||||
class Style:
|
||||
RESET_ALL = ""
|
||||
HAS_COLORAMA = False
|
||||
|
||||
# API 基础 URL
|
||||
BASE_URL = "http://localhost:8080/api/v1"
|
||||
|
||||
|
||||
def print_colored(text, color=Fore.RESET):
|
||||
"""打印彩色文本"""
|
||||
print(f"{color}{text}{Style.RESET_ALL}")
|
||||
|
||||
|
||||
def print_header(text):
|
||||
"""打印标题"""
|
||||
print_colored(f"\n{'=' * 40}", Fore.CYAN)
|
||||
print_colored(f" {text}", Fore.CYAN)
|
||||
print_colored(f"{'=' * 40}\n", Fore.CYAN)
|
||||
|
||||
|
||||
def print_step(text):
|
||||
"""打印步骤标题"""
|
||||
print_colored(f"\n=== {text} ===", Fore.YELLOW)
|
||||
|
||||
|
||||
def print_success(text):
|
||||
"""打印成功消息"""
|
||||
print_colored(f"✓ {text}", Fore.GREEN)
|
||||
|
||||
|
||||
def print_error(text):
|
||||
"""打印错误消息"""
|
||||
print_colored(f"✗ {text}", Fore.RED)
|
||||
|
||||
|
||||
def print_info(text):
|
||||
"""打印信息消息"""
|
||||
print_colored(f" {text}", Fore.GRAY)
|
||||
|
||||
|
||||
def register_user():
|
||||
"""步骤1: 注册新用户"""
|
||||
print_step("步骤1: 注册新用户")
|
||||
|
||||
random_num = random.randint(10000, 99999)
|
||||
username = f"testuser{random_num}"
|
||||
email = f"test{random_num}@example.com"
|
||||
login_password = "password123"
|
||||
verification_code = "123456"
|
||||
|
||||
print_info(f"用户名: {username}")
|
||||
print_info(f"邮箱: {email}")
|
||||
print_info(f"密码: {login_password}")
|
||||
|
||||
register_data = {
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": login_password,
|
||||
"verification_code": verification_code
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/auth/register",
|
||||
json=register_data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") == 200:
|
||||
jwt_token = result["data"]["token"]
|
||||
user_id = result["data"]["user_info"]["id"]
|
||||
user_role = result["data"]["user_info"]["role"]
|
||||
|
||||
print_success("注册成功!")
|
||||
print_info(f"用户ID: {user_id}")
|
||||
print_info(f"角色: {user_role}")
|
||||
|
||||
return {
|
||||
"username": username,
|
||||
"email": email,
|
||||
"login_password": login_password,
|
||||
"jwt_token": jwt_token,
|
||||
"user_id": user_id,
|
||||
"user_role": user_role
|
||||
}
|
||||
else:
|
||||
print_error(f"注册失败: {result.get('message', '未知错误')}")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print_error(f"注册失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_profile(jwt_token):
|
||||
"""步骤2: 创建Minecraft角色"""
|
||||
print_step("步骤2: 创建Minecraft角色")
|
||||
|
||||
profile_name = f"TestPlayer{random.randint(100, 999)}"
|
||||
print_info(f"角色名: {profile_name}")
|
||||
|
||||
profile_data = {
|
||||
"name": profile_name
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {jwt_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/profile/",
|
||||
json=profile_data,
|
||||
headers=headers,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") == 200:
|
||||
profile_uuid = result["data"]["uuid"]
|
||||
print_success("角色创建成功!")
|
||||
print_info(f"角色UUID: {profile_uuid}")
|
||||
print_info(f"角色名: {result['data']['name']}")
|
||||
|
||||
return {
|
||||
"profile_name": profile_name,
|
||||
"profile_uuid": profile_uuid
|
||||
}
|
||||
else:
|
||||
print_error(f"角色创建失败: {result.get('message', '未知错误')}")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print_error(f"角色创建失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def reset_yggdrasil_password(jwt_token):
|
||||
"""步骤3: 重置并获取Yggdrasil密码"""
|
||||
print_step("步骤3: 重置并获取Yggdrasil密码")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {jwt_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/user/yggdrasil-password/reset",
|
||||
headers=headers,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") == 200:
|
||||
yggdrasil_password = result["data"]["password"]
|
||||
print_success("Yggdrasil密码重置成功!")
|
||||
print_info(f"Yggdrasil密码: {yggdrasil_password}")
|
||||
return yggdrasil_password
|
||||
else:
|
||||
print_error(f"Yggdrasil密码重置失败: {result.get('message', '未知错误')}")
|
||||
return ""
|
||||
except requests.exceptions.RequestException as e:
|
||||
print_error(f"Yggdrasil密码重置失败: {str(e)}")
|
||||
return ""
|
||||
|
||||
|
||||
def generate_output(user_info, profile_info, yggdrasil_password):
|
||||
"""生成输出信息"""
|
||||
output = f"""========================================
|
||||
CarrotSkin 测试账户信息
|
||||
========================================
|
||||
|
||||
=== 账户信息 ===
|
||||
用户名: {user_info['username']}
|
||||
邮箱: {user_info['email']}
|
||||
登录密码: {user_info['login_password']}
|
||||
用户ID: {user_info['user_id']}
|
||||
|
||||
=== JWT Token (API认证) ===
|
||||
Token: {user_info['jwt_token']}
|
||||
|
||||
=== 角色信息 ===
|
||||
角色名: {profile_info['profile_name']}
|
||||
角色UUID: {profile_info['profile_uuid']}
|
||||
|
||||
=== Yggdrasil信息 ===
|
||||
Yggdrasil密码: {yggdrasil_password}
|
||||
|
||||
=== 测试命令 ===
|
||||
|
||||
# 1. API登录
|
||||
curl -X POST http://localhost:8080/api/v1/auth/login \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '{{"username":"{user_info['username']}","password":"{user_info['login_password']}"}}'
|
||||
|
||||
# 2. 创建角色
|
||||
curl -X POST http://localhost:8080/api/v1/profile/ \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H 'Authorization: Bearer {user_info['jwt_token']}' \\
|
||||
-d '{{"name":"NewProfile"}}'
|
||||
|
||||
# 3. 重置Yggdrasil密码
|
||||
curl -X POST http://localhost:8080/api/v1/user/yggdrasil-password/reset \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H 'Authorization: Bearer {user_info['jwt_token']}'
|
||||
|
||||
# 4. Yggdrasil认证
|
||||
curl -X POST http://localhost:8080/api/v1/yggdrasil/authserver/authenticate \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '{{
|
||||
"username": "{user_info['username']}",
|
||||
"password": "{yggdrasil_password}",
|
||||
"requestUser": true
|
||||
}}'
|
||||
|
||||
# 5. 获取角色信息
|
||||
curl -X GET http://localhost:8080/api/v1/profile/{profile_info['profile_uuid']}
|
||||
|
||||
========================================
|
||||
"""
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print_header("CarrotSkin 测试账户生成器")
|
||||
|
||||
# 步骤1: 注册用户
|
||||
user_info = register_user()
|
||||
|
||||
# 步骤2: 创建角色
|
||||
profile_info = create_profile(user_info["jwt_token"])
|
||||
|
||||
# 步骤3: 重置Yggdrasil密码
|
||||
yggdrasil_password = reset_yggdrasil_password(user_info["jwt_token"])
|
||||
|
||||
# 步骤4: 输出所有信息
|
||||
print_header("测试账户信息汇总")
|
||||
|
||||
output = generate_output(user_info, profile_info, yggdrasil_password)
|
||||
print(output)
|
||||
|
||||
# 保存到文件
|
||||
output_file = f"test_account_{user_info['username']}.txt"
|
||||
try:
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(output)
|
||||
print_success(f"信息已保存到文件: {output_file}")
|
||||
except Exception as e:
|
||||
print_error(f"保存文件失败: {str(e)}")
|
||||
|
||||
print_header("测试完成!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
4
start.sh
4
start.sh
@@ -5,9 +5,9 @@
|
||||
# 设置环境变量
|
||||
export DATABASE_HOST=192.168.10.205
|
||||
export DATABASE_PORT=5432
|
||||
export DATABASE_USERNAME=skin
|
||||
export DATABASE_USERNAME=skin3
|
||||
export DATABASE_PASSWORD=lanyimin123
|
||||
export DATABASE_NAME=skin
|
||||
export DATABASE_NAME=skin3
|
||||
export DATABASE_SSL_MODE=disable
|
||||
export DATABASE_TIMEZONE=Asia/Shanghai
|
||||
|
||||
|
||||
Reference in New Issue
Block a user