feat: 添加Yggdrasil密码重置功能,更新依赖和配置
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,7 +23,8 @@ dist/
|
|||||||
build/
|
build/
|
||||||
|
|
||||||
# Compiled binaries
|
# Compiled binaries
|
||||||
server
|
/server
|
||||||
|
server.exe
|
||||||
|
|
||||||
# IDE files
|
# IDE files
|
||||||
.vscode/
|
.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/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
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/minio/minio-go/v7 v7.0.66
|
||||||
github.com/redis/go-redis/v9 v9.0.5
|
github.com/redis/go-redis/v9 v9.0.5
|
||||||
github.com/spf13/viper v1.21.0
|
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-assets v1.0.7
|
||||||
github.com/wenlng/go-captcha/v2 v2.0.4
|
github.com/wenlng/go-captcha/v2 v2.0.4
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
gorm.io/driver/postgres v1.5.4
|
gorm.io/datatypes v1.2.7
|
||||||
gorm.io/gorm v1.25.5
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/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/image v0.16.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
gorm.io/driver/mysql v1.5.6 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -47,10 +54,10 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.15.1 // indirect
|
github.com/go-playground/validator/v10 v10.15.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // 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/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.4.3
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // 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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
github.com/PuerkitoBio/purell v1.1.1 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/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 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
|
||||||
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
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 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
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=
|
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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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/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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
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/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 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
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-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/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 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
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=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
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=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ func RegisterRoutes(router *gin.Engine) {
|
|||||||
|
|
||||||
// 更换邮箱
|
// 更换邮箱
|
||||||
userGroup.POST("/change-email", ChangeEmail)
|
userGroup.POST("/change-email", ChangeEmail)
|
||||||
|
|
||||||
|
// Yggdrasil密码相关
|
||||||
|
userGroup.POST("/yggdrasil-password/reset", ResetYggdrasilPassword) // 重置Yggdrasil密码并返回新密码
|
||||||
}
|
}
|
||||||
|
|
||||||
// 材质路由
|
// 材质路由
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"carrotskin/internal/service"
|
"carrotskin/internal/service"
|
||||||
"carrotskin/internal/types"
|
"carrotskin/internal/types"
|
||||||
"carrotskin/pkg/config"
|
"carrotskin/pkg/config"
|
||||||
|
"carrotskin/pkg/database"
|
||||||
"carrotskin/pkg/logger"
|
"carrotskin/pkg/logger"
|
||||||
"carrotskin/pkg/redis"
|
"carrotskin/pkg/redis"
|
||||||
"carrotskin/pkg/storage"
|
"carrotskin/pkg/storage"
|
||||||
@@ -413,3 +414,49 @@ func ChangeEmail(c *gin.Context) {
|
|||||||
UpdatedAt: user.UpdatedAt,
|
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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,24 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/datatypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User 用户模型
|
// User 用户模型
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"`
|
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"`
|
||||||
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端
|
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端
|
||||||
Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex" json:"email"`
|
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"`
|
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"`
|
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"`
|
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:删除
|
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类型
|
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"`
|
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"`
|
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"`
|
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
|
|||||||
@@ -14,3 +14,9 @@ func GetYggdrasilPasswordById(Id int64) (string, error) {
|
|||||||
}
|
}
|
||||||
return yggdrasil.Password, nil
|
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,
|
redisDataJSON,
|
||||||
expireTime,
|
expireTime,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return "", "", "", 0, fmt.Errorf("存储验证码到Redis失败: %w", err)
|
return "", "", "", 0, fmt.Errorf("存储验证码到redis失败: %w", err)
|
||||||
}
|
}
|
||||||
return mBase64, tBase64, captchaID, y - 10, nil
|
return mBase64, tBase64, captchaID, y - 10, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyCaptchaData 验证用户验证码
|
// VerifyCaptchaData 验证用户验证码
|
||||||
func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, id string) (bool, error) {
|
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
|
redisKey := redisKeyPrefix + id
|
||||||
|
|
||||||
// 从Redis获取验证信息,使用注入的客户端
|
// 从Redis获取验证信息,使用注入的客户端
|
||||||
@@ -144,11 +150,11 @@ func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, i
|
|||||||
if redisClient.Nil(err) { // 使用封装客户端的Nil错误
|
if redisClient.Nil(err) { // 使用封装客户端的Nil错误
|
||||||
return false, errors.New("验证码已过期或无效")
|
return false, errors.New("验证码已过期或无效")
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("Redis查询失败: %w", err)
|
return false, fmt.Errorf("redis查询失败: %w", err)
|
||||||
}
|
}
|
||||||
var redisData RedisData
|
var redisData RedisData
|
||||||
if err := json.Unmarshal([]byte(dataJSON), &redisData); err != nil {
|
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
|
tx := redisData.Tx
|
||||||
ty := redisData.Ty
|
ty := redisData.Ty
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import (
|
|||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/pkg/redis"
|
"carrotskin/pkg/redis"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"go.uber.org/zap"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ func TestSerializeUser_NilUser(t *testing.T) {
|
|||||||
func TestSerializeUser_ActualCall(t *testing.T) {
|
func TestSerializeUser_ActualCall(t *testing.T) {
|
||||||
logger := zaptest.NewLogger(t)
|
logger := zaptest.NewLogger(t)
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
Properties: "{}",
|
// Properties 使用 datatypes.JSON,测试中可以为空
|
||||||
}
|
}
|
||||||
|
|
||||||
result := SerializeUser(logger, user, "test-uuid-123")
|
result := SerializeUser(logger, user, "test-uuid-123")
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
|
|||||||
Role: "user",
|
Role: "user",
|
||||||
Status: 1,
|
Status: 1,
|
||||||
Points: 0, // 初始积分可以从配置读取
|
Points: 0, // 初始积分可以从配置读取
|
||||||
|
// Properties 字段使用 datatypes.JSON,默认为 nil,数据库会存储 NULL
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repository.CreateUser(user); err != nil {
|
if err := repository.CreateUser(user); err != nil {
|
||||||
|
|||||||
@@ -7,18 +7,19 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"carrotskin/pkg/config"
|
||||||
"carrotskin/pkg/email"
|
"carrotskin/pkg/email"
|
||||||
"carrotskin/pkg/redis"
|
"carrotskin/pkg/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// 验证码类型
|
// 验证码类型
|
||||||
VerificationTypeRegister = "register"
|
VerificationTypeRegister = "register"
|
||||||
VerificationTypeResetPassword = "reset_password"
|
VerificationTypeResetPassword = "reset_password"
|
||||||
VerificationTypeChangeEmail = "change_email"
|
VerificationTypeChangeEmail = "change_email"
|
||||||
|
|
||||||
// 验证码配置
|
// 验证码配置
|
||||||
CodeLength = 6 // 验证码长度
|
CodeLength = 6 // 验证码长度
|
||||||
CodeExpiration = 10 * time.Minute // 验证码有效期
|
CodeExpiration = 10 * time.Minute // 验证码有效期
|
||||||
CodeRateLimit = 1 * time.Minute // 发送频率限制
|
CodeRateLimit = 1 * time.Minute // 发送频率限制
|
||||||
)
|
)
|
||||||
@@ -39,6 +40,12 @@ func GenerateVerificationCode() (string, error) {
|
|||||||
|
|
||||||
// SendVerificationCode 发送验证码
|
// SendVerificationCode 发送验证码
|
||||||
func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailService *email.Service, email, codeType string) error {
|
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)
|
rateLimitKey := fmt.Sprintf("verification:rate_limit:%s:%s", codeType, email)
|
||||||
exists, err := redisClient.Exists(ctx, rateLimitKey)
|
exists, err := redisClient.Exists(ctx, rateLimitKey)
|
||||||
@@ -78,6 +85,12 @@ func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailS
|
|||||||
|
|
||||||
// VerifyCode 验证验证码
|
// VerifyCode 验证验证码
|
||||||
func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, codeType string) error {
|
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)
|
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
|
||||||
|
|
||||||
// 从Redis获取验证码
|
// 从Redis获取验证码
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go.uber.org/zap"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,6 +79,33 @@ func GetPasswordByUserId(db *gorm.DB, userId int64) (string, error) {
|
|||||||
return passwordStore, nil
|
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 记录玩家加入服务器的会话信息
|
// JoinServer 记录玩家加入服务器的会话信息
|
||||||
func JoinServer(db *gorm.DB, logger *zap.Logger, redisClient *redis.Client, serverId, accessToken, selectedProfile, ip string) error {
|
func JoinServer(db *gorm.DB, logger *zap.Logger, redisClient *redis.Client, serverId, accessToken, selectedProfile, ip string) error {
|
||||||
// 输入验证
|
// 输入验证
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ import (
|
|||||||
|
|
||||||
// Config 应用配置结构体
|
// Config 应用配置结构体
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `mapstructure:"server"`
|
Environment string `mapstructure:"environment"`
|
||||||
Database DatabaseConfig `mapstructure:"database"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
Redis RedisConfig `mapstructure:"redis"`
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
RustFS RustFSConfig `mapstructure:"rustfs"`
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
JWT JWTConfig `mapstructure:"jwt"`
|
RustFS RustFSConfig `mapstructure:"rustfs"`
|
||||||
Casbin CasbinConfig `mapstructure:"casbin"`
|
JWT JWTConfig `mapstructure:"jwt"`
|
||||||
Log LogConfig `mapstructure:"log"`
|
Casbin CasbinConfig `mapstructure:"casbin"`
|
||||||
Upload UploadConfig `mapstructure:"upload"`
|
Log LogConfig `mapstructure:"log"`
|
||||||
Email EmailConfig `mapstructure:"email"`
|
Upload UploadConfig `mapstructure:"upload"`
|
||||||
|
Email EmailConfig `mapstructure:"email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig 服务器配置
|
// ServerConfig 服务器配置
|
||||||
@@ -78,21 +79,21 @@ type CasbinConfig struct {
|
|||||||
|
|
||||||
// LogConfig 日志配置
|
// LogConfig 日志配置
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
Level string `mapstructure:"level"`
|
Level string `mapstructure:"level"`
|
||||||
Format string `mapstructure:"format"`
|
Format string `mapstructure:"format"`
|
||||||
Output string `mapstructure:"output"`
|
Output string `mapstructure:"output"`
|
||||||
MaxSize int `mapstructure:"max_size"`
|
MaxSize int `mapstructure:"max_size"`
|
||||||
MaxBackups int `mapstructure:"max_backups"`
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
MaxAge int `mapstructure:"max_age"`
|
MaxAge int `mapstructure:"max_age"`
|
||||||
Compress bool `mapstructure:"compress"`
|
Compress bool `mapstructure:"compress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadConfig 文件上传配置
|
// UploadConfig 文件上传配置
|
||||||
type UploadConfig struct {
|
type UploadConfig struct {
|
||||||
MaxSize int64 `mapstructure:"max_size"`
|
MaxSize int64 `mapstructure:"max_size"`
|
||||||
AllowedTypes []string `mapstructure:"allowed_types"`
|
AllowedTypes []string `mapstructure:"allowed_types"`
|
||||||
TextureMaxSize int64 `mapstructure:"texture_max_size"`
|
TextureMaxSize int64 `mapstructure:"texture_max_size"`
|
||||||
AvatarMaxSize int64 `mapstructure:"avatar_max_size"`
|
AvatarMaxSize int64 `mapstructure:"avatar_max_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmailConfig 邮件配置
|
// EmailConfig 邮件配置
|
||||||
@@ -301,4 +302,14 @@ func overrideFromEnv(config *Config) {
|
|||||||
if emailEnabled := os.Getenv("EMAIL_ENABLED"); emailEnabled != "" {
|
if emailEnabled := os.Getenv("EMAIL_ENABLED"); emailEnabled != "" {
|
||||||
config.Email.Enabled = emailEnabled == "true" || emailEnabled == "True" || emailEnabled == "TRUE" || emailEnabled == "1"
|
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("开始执行数据库迁移...")
|
logger.Info("开始执行数据库迁移...")
|
||||||
|
|
||||||
// 迁移所有表 - 注意顺序:先创建被引用的表,再创建引用表
|
// 迁移所有表 - 注意顺序:先创建被引用的表,再创建引用表
|
||||||
err = db.AutoMigrate(
|
// 使用分批迁移,避免某些表的问题影响其他表
|
||||||
|
tables := []interface{}{
|
||||||
// 用户相关表(先创建,因为其他表可能引用它)
|
// 用户相关表(先创建,因为其他表可能引用它)
|
||||||
&model.User{},
|
&model.User{},
|
||||||
&model.UserPointLog{},
|
&model.UserPointLog{},
|
||||||
@@ -87,11 +88,30 @@ func AutoMigrate(logger *zap.Logger) error {
|
|||||||
|
|
||||||
// Casbin权限规则表
|
// Casbin权限规则表
|
||||||
&model.CasbinRule{},
|
&model.CasbinRule{},
|
||||||
)
|
}
|
||||||
|
|
||||||
if err != nil {
|
// 逐个迁移表,以便更好地定位问题
|
||||||
logger.Error("数据库迁移失败", zap.Error(err))
|
for _, table := range tables {
|
||||||
return fmt.Errorf("数据库迁移失败: %w", err)
|
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("数据库迁移完成")
|
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_HOST=192.168.10.205
|
||||||
export DATABASE_PORT=5432
|
export DATABASE_PORT=5432
|
||||||
export DATABASE_USERNAME=skin
|
export DATABASE_USERNAME=skin3
|
||||||
export DATABASE_PASSWORD=lanyimin123
|
export DATABASE_PASSWORD=lanyimin123
|
||||||
export DATABASE_NAME=skin
|
export DATABASE_NAME=skin3
|
||||||
export DATABASE_SSL_MODE=disable
|
export DATABASE_SSL_MODE=disable
|
||||||
export DATABASE_TIMEZONE=Asia/Shanghai
|
export DATABASE_TIMEZONE=Asia/Shanghai
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user