diff --git a/.gitignore b/.gitignore index 8db8744..b50e444 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,8 @@ dist/ build/ # Compiled binaries -server +/server +server.exe # IDE files .vscode/ diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..819b7ec --- /dev/null +++ b/cmd/server/main.go @@ -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("服务器已关闭") +} diff --git a/go.mod b/go.mod index 8c1e2a0..377b009 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 3dae08b..bf85062 100644 --- a/go.sum +++ b/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= diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 465793a..95cee4c 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -37,6 +37,9 @@ func RegisterRoutes(router *gin.Engine) { // 更换邮箱 userGroup.POST("/change-email", ChangeEmail) + + // Yggdrasil密码相关 + userGroup.POST("/yggdrasil-password/reset", ResetYggdrasilPassword) // 重置Yggdrasil密码并返回新密码 } // 材质路由 diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index c72d48a..86a6068 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -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, + })) +} diff --git a/internal/model/user.go b/internal/model/user.go index 68bd160..679e3b5 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -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 指定表名 diff --git a/internal/repository/yggdrasil_repository.go b/internal/repository/yggdrasil_repository.go index 1745beb..49620e3 100644 --- a/internal/repository/yggdrasil_repository.go +++ b/internal/repository/yggdrasil_repository.go @@ -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 +} \ No newline at end of file diff --git a/internal/service/captcha_service.go b/internal/service/captcha_service.go index 325b24e..78fa7a0 100644 --- a/internal/service/captcha_service.go +++ b/internal/service/captcha_service.go @@ -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 diff --git a/internal/service/serialize_service.go b/internal/service/serialize_service.go index 61cc510..8feb83e 100644 --- a/internal/service/serialize_service.go +++ b/internal/service/serialize_service.go @@ -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" ) diff --git a/internal/service/serialize_service_test.go b/internal/service/serialize_service_test.go index 206b964..4f2d3be 100644 --- a/internal/service/serialize_service_test.go +++ b/internal/service/serialize_service_test.go @@ -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") diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 0269619..e1d6ff3 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -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 { diff --git a/internal/service/verification_service.go b/internal/service/verification_service.go index 991d15c..49bb40d 100644 --- a/internal/service/verification_service.go +++ b/internal/service/verification_service.go @@ -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 { diff --git a/internal/service/yggdrasil_service.go b/internal/service/yggdrasil_service.go index 5c89e74..0b5d3c1 100644 --- a/internal/service/yggdrasil_service.go +++ b/internal/service/yggdrasil_service.go @@ -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 { // 输入验证 diff --git a/pkg/config/config.go b/pkg/config/config.go index 0d8ebcb..1cf5dd8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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" } diff --git a/pkg/database/manager.go b/pkg/database/manager.go index 59ce434..d17f916 100644 --- a/pkg/database/manager.go +++ b/pkg/database/manager.go @@ -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("数据库迁移完成") diff --git a/scripts/generate_test_account.py b/scripts/generate_test_account.py new file mode 100644 index 0000000..32199d0 --- /dev/null +++ b/scripts/generate_test_account.py @@ -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() + diff --git a/start.sh b/start.sh index 1db3343..9466247 100755 --- a/start.sh +++ b/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