From c1d5b66f4962bd8b9bbc1859d5a03b966cfa804a Mon Sep 17 00:00:00 2001 From: LinXin Date: Mon, 3 Nov 2025 20:06:37 +0800 Subject: [PATCH 1/3] feat: auth system with middleware and auth APIs --- backend/database/seed.go | 38 ++- backend/go.mod | 39 ++- backend/go.sum | 91 ++++++- backend/handlers/auth/login.go | 69 +++++ backend/handlers/auth/register.go | 71 +++++ backend/main.go | 11 + backend/middleware/auth.go | 46 ++++ backend/routes/routes.go | 54 ++++ backend/services/auth_service.go | 149 +++++++++++ backend/services/errors.go | 14 + backend/utils/jwt.go | 57 ++++ backend/utils/password.go | 18 ++ ...01\344\270\255\351\227\264\344\273\266.md" | 247 ++++++++++++++++++ ...37\345\210\235\345\247\213\345\214\226.md" | 6 + 14 files changed, 883 insertions(+), 27 deletions(-) create mode 100644 backend/handlers/auth/login.go create mode 100644 backend/handlers/auth/register.go create mode 100644 backend/middleware/auth.go create mode 100644 backend/routes/routes.go create mode 100644 backend/services/auth_service.go create mode 100644 backend/services/errors.go create mode 100644 backend/utils/jwt.go create mode 100644 backend/utils/password.go create mode 100644 "docs/\347\231\273\345\275\225\346\263\250\345\206\214/JWT\350\256\244\350\257\201\345\222\214\351\252\214\350\257\201\344\270\255\351\227\264\344\273\266.md" create mode 100644 "docs/\347\263\273\347\273\237\345\210\235\345\247\213\345\214\226.md" diff --git a/backend/database/seed.go b/backend/database/seed.go index 991a3ff..7ad49d1 100644 --- a/backend/database/seed.go +++ b/backend/database/seed.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "progress-wall-backend/models" + "progress-wall-backend/utils" "gorm.io/gorm" ) @@ -27,24 +28,29 @@ func Seed(db *gorm.DB) error { } // createDefaultUsers 创建默认用户 -// 该函数负责在系统初始化时创建默认的管理员用户和普通用户 +// 该函数负责在系统初始化时创建默认的管理员用户 // 参数: db - 数据库连接实例 // 返回: error - 如果创建失败则返回错误,否则返回nil func createDefaultUsers(db *gorm.DB) error { - // 检查是否已存在默认用户 - var count int64 - db.Model(&models.User{}).Count(&count) - if count > 0 { - log.Println("用户表不为空,跳过默认用户创建") + // 检查admin用户是否已存在 + var existingUser models.User + result := db.Where("username = ?", "admin").First(&existingUser) + if result.Error == nil { + log.Println("admin用户已存在,跳过创建") return nil } + // 加密管理员密码 + adminPasswordHash, err := utils.HashPassword("admin123") + if err != nil { + return fmt.Errorf("加密管理员密码失败: %w", err) + } + // 创建默认管理员用户 adminUser := models.User{ - ID: 1, Username: "admin", Email: "admin@example.com", - Password: "admin123", // TODO: 等待JWT完成后替换为加密密码 + Password: adminPasswordHash, Nickname: "系统管理员", Status: models.UserStatusEnabled, } @@ -53,21 +59,7 @@ func createDefaultUsers(db *gorm.DB) error { return fmt.Errorf("创建默认管理员用户失败: %w", err) } - // 创建默认普通用户 - guestUser := models.User{ - ID: 2, - Username: "guest", - Email: "guest@example.com", - Password: "guest123", // TODO: 等待JWT完成后替换为加密密码 - Nickname: "访客用户", - Status: models.UserStatusEnabled, - } - - if err := db.Create(&guestUser).Error; err != nil { - return fmt.Errorf("创建默认访客用户失败: %w", err) - } - - log.Println("默认用户创建成功: admin, guest") + log.Println("默认用户创建成功: admin") return nil } diff --git a/backend/go.mod b/backend/go.mod index ee2715d..b4a7880 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,18 +1,53 @@ module progress-wall-backend -go 1.21 +go 1.24.0 + +toolchain go1.24.9 require ( github.com/joho/godotenv v1.4.0 - golang.org/x/crypto v0.15.0 gorm.io/driver/mysql v1.5.2 gorm.io/driver/sqlite v1.5.4 gorm.io/gorm v1.25.5 ) require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/cors v1.7.6 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index fba38f4..c2b5d08 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,15 +1,102 @@ +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +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.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= diff --git a/backend/handlers/auth/login.go b/backend/handlers/auth/login.go new file mode 100644 index 0000000..b086d9a --- /dev/null +++ b/backend/handlers/auth/login.go @@ -0,0 +1,69 @@ +package auth + +import ( + "net/http" + "progress-wall-backend/config" + "progress-wall-backend/models" + "progress-wall-backend/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// LoginHandler 登录处理器 +type LoginHandler struct { + authService *services.AuthService +} + +// NewLoginHandler 创建登录处理器 +func NewLoginHandler(db *gorm.DB, cfg *config.Config) *LoginHandler { + return &LoginHandler{ + authService: services.NewAuthService(db, cfg), + } +} + +// LoginRequest 登录请求结构 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// LoginResponse 登录响应结构 +type LoginResponse struct { + AccessToken string `json:"accessToken"` + User *models.User `json:"user,omitempty"` +} + +// Login 处理登录请求 +func (h *LoginHandler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + // 调用service层处理业务逻辑 + result, err := h.authService.Login(services.LoginRequest{ + Username: req.Username, + Password: req.Password, + }) + + if err != nil { + // 根据错误类型返回相应的HTTP状态码 + switch err { + case services.ErrInvalidCredentials: + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + case services.ErrUserDisabled: + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + + // 返回成功响应 + c.JSON(http.StatusOK, LoginResponse{ + AccessToken: result.AccessToken, + User: result.User, + }) +} diff --git a/backend/handlers/auth/register.go b/backend/handlers/auth/register.go new file mode 100644 index 0000000..d44e5c5 --- /dev/null +++ b/backend/handlers/auth/register.go @@ -0,0 +1,71 @@ +package auth + +import ( + "net/http" + "progress-wall-backend/config" + "progress-wall-backend/models" + "progress-wall-backend/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// RegisterHandler 注册处理器 +type RegisterHandler struct { + authService *services.AuthService +} + +// NewRegisterHandler 创建注册处理器 +func NewRegisterHandler(db *gorm.DB, cfg *config.Config) *RegisterHandler { + return &RegisterHandler{ + authService: services.NewAuthService(db, cfg), + } +} + +// RegisterRequest 注册请求结构 +type RegisterRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Nickname string `json:"nickname"` +} + +// RegisterResponse 注册响应结构 +type RegisterResponse struct { + User *models.User `json:"user"` +} + +// Register 处理注册请求 +func (h *RegisterHandler) Register(c *gin.Context) { + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + // 调用service层处理业务逻辑 + result, err := h.authService.Register(services.RegisterRequest{ + Username: req.Username, + Email: req.Email, + Password: req.Password, + Nickname: req.Nickname, + }) + + if err != nil { + // 根据错误类型返回相应的HTTP状态码 + switch err { + case services.ErrUserExists: + c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + case services.ErrInvalidPassword: + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + + // 返回成功响应 + c.JSON(http.StatusCreated, RegisterResponse{ + User: result.User, + }) +} diff --git a/backend/main.go b/backend/main.go index 59061af..7e7307a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -5,6 +5,7 @@ import ( "log" "progress-wall-backend/config" "progress-wall-backend/database" + "progress-wall-backend/routes" ) func main() { @@ -37,4 +38,14 @@ func main() { } log.Println("数据库初始化完成") + + // 设置路由 + r := routes.SetupRoutes(db, cfg) + + // 启动HTTP服务器 + addr := fmt.Sprintf(":%s", cfg.Server.Port) + log.Printf("服务器启动在端口 %s\n", cfg.Server.Port) + if err := r.Run(addr); err != nil { + log.Fatal("服务器启动失败:", err) + } } diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go new file mode 100644 index 0000000..be457e6 --- /dev/null +++ b/backend/middleware/auth.go @@ -0,0 +1,46 @@ +package middleware + +import ( + "net/http" + "progress-wall-backend/config" + "progress-wall-backend/utils" + "strings" + + "github.com/gin-gonic/gin" +) + +// AuthMiddleware JWT认证中间件 +func AuthMiddleware(cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + // 从请求头获取Authorization + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证token"}) + c.Abort() + return + } + + // 检查Bearer前缀 + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "认证token格式错误"}) + c.Abort() + return + } + + // 验证token + tokenString := parts[1] + claims, err := utils.ValidateToken(tokenString, cfg) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"}) + c.Abort() + return + } + + // 将用户信息存储到上下文 + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + + c.Next() + } +} diff --git a/backend/routes/routes.go b/backend/routes/routes.go new file mode 100644 index 0000000..bbb14dc --- /dev/null +++ b/backend/routes/routes.go @@ -0,0 +1,54 @@ +package routes + +import ( + "progress-wall-backend/config" + "progress-wall-backend/handlers/auth" + "progress-wall-backend/middleware" + "strings" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// SetupRoutes 设置路由 +func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { + // 根据配置设置Gin模式 + if cfg.Server.Mode == "release" { + gin.SetMode(gin.ReleaseMode) + } + + r := gin.Default() + + // 配置CORS + corsConfig := cors.DefaultConfig() + corsConfig.AllowOrigins = strings.Split(cfg.CORS.AllowOrigins, ",") + corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"} + corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"} + corsConfig.AllowCredentials = true + r.Use(cors.New(corsConfig)) + + // 初始化处理器 + loginHandler := auth.NewLoginHandler(db, cfg) + registerHandler := auth.NewRegisterHandler(db, cfg) + + // 公开路由(不需要认证) + api := r.Group("/api") + { + authGroup := api.Group("/auth") + { + authGroup.POST("/login", loginHandler.Login) + authGroup.POST("/register", registerHandler.Register) + } + } + + // 受保护的路由(需要认证) + protected := api.Group("") + protected.Use(middleware.AuthMiddleware(cfg)) + { + // 这里可以添加需要认证的路由 + // 例如: protected.GET("/profile", userHandler.GetProfile) + } + + return r +} diff --git a/backend/services/auth_service.go b/backend/services/auth_service.go new file mode 100644 index 0000000..52c666f --- /dev/null +++ b/backend/services/auth_service.go @@ -0,0 +1,149 @@ +package services + +import ( + "errors" + "progress-wall-backend/config" + "progress-wall-backend/models" + "progress-wall-backend/utils" + "time" + + "gorm.io/gorm" +) + +// AuthService 认证服务 +type AuthService struct { + db *gorm.DB + cfg *config.Config +} + +// NewAuthService 创建认证服务 +func NewAuthService(db *gorm.DB, cfg *config.Config) *AuthService { + return &AuthService{ + db: db, + cfg: cfg, + } +} + +// LoginRequest 登录请求结构 +type LoginRequest struct { + Username string + Password string +} + +// LoginResult 登录结果 +type LoginResult struct { + AccessToken string + User *models.User +} + +// Login 处理登录业务逻辑 +func (s *AuthService) Login(req LoginRequest) (*LoginResult, error) { + // 查找用户(支持用户名或邮箱登录) + var user models.User + result := s.db.Where("username = ? OR email = ?", req.Username, req.Username). + First(&user) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrInvalidCredentials + } + return nil, ErrUserNotFound + } + + // 检查用户状态 + if user.Status != models.UserStatusEnabled { + return nil, ErrUserDisabled + } + + // 验证密码 + if !utils.CheckPasswordHash(req.Password, user.Password) { + return nil, ErrInvalidCredentials + } + + // 生成JWT token + token, err := utils.GenerateToken(user.ID, user.Username, s.cfg) + if err != nil { + return nil, ErrGenerateToken + } + + // 更新最后登录时间 + now := time.Now() + user.LastLogin = &now + if err := s.db.Save(&user).Error; err != nil { + return nil, ErrUpdateLoginTime + } + + // 清除敏感信息 + user.Password = "" + + return &LoginResult{ + AccessToken: token, + User: &user, + }, nil +} + +// RegisterRequest 注册请求结构 +type RegisterRequest struct { + Username string + Email string + Password string + Nickname string +} + +// RegisterResult 注册结果 +type RegisterResult struct { + User *models.User +} + +// Register 处理注册业务逻辑 +func (s *AuthService) Register(req RegisterRequest) (*RegisterResult, error) { + // 检查用户名是否已存在 + var existingUser models.User + result := s.db.Where("username = ?", req.Username).First(&existingUser) + if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, errors.New("查询用户失败") + } + if result.Error == nil { + return nil, ErrUserExists + } + + // 检查邮箱是否已存在 + result = s.db.Where("email = ?", req.Email).First(&existingUser) + if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, errors.New("查询用户失败") + } + if result.Error == nil { + return nil, ErrUserExists + } + + // 验证密码长度(至少6位) + if len(req.Password) < 6 { + return nil, ErrInvalidPassword + } + + // 加密密码 + hashedPassword, err := utils.HashPassword(req.Password) + if err != nil { + return nil, errors.New("加密密码失败") + } + + // 创建新用户 + user := models.User{ + Username: req.Username, + Email: req.Email, + Password: hashedPassword, + Nickname: req.Nickname, + Status: models.UserStatusEnabled, + } + + if err := s.db.Create(&user).Error; err != nil { + return nil, errors.New("创建用户失败") + } + + // 清除敏感信息 + user.Password = "" + + return &RegisterResult{ + User: &user, + }, nil +} diff --git a/backend/services/errors.go b/backend/services/errors.go new file mode 100644 index 0000000..efbe94c --- /dev/null +++ b/backend/services/errors.go @@ -0,0 +1,14 @@ +package services + +import "errors" + +// 定义业务错误 +var ( + ErrInvalidCredentials = errors.New("用户名或密码错误") + ErrUserDisabled = errors.New("账户已被禁用") + ErrUserNotFound = errors.New("用户不存在") + ErrGenerateToken = errors.New("生成token失败") + ErrUpdateLoginTime = errors.New("更新登录时间失败") + ErrUserExists = errors.New("用户名或邮箱已存在") + ErrInvalidPassword = errors.New("密码格式不正确") +) diff --git a/backend/utils/jwt.go b/backend/utils/jwt.go new file mode 100644 index 0000000..354ca44 --- /dev/null +++ b/backend/utils/jwt.go @@ -0,0 +1,57 @@ +package utils + +import ( + "errors" + "progress-wall-backend/config" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// Claims JWT声明结构 +type Claims struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +// GenerateToken 生成JWT token +func GenerateToken(userID uint, username string, cfg *config.Config) (string, error) { + claims := &Claims{ + UserID: userID, + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(cfg.JWT.ExpireHours) * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(cfg.JWT.Secret)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// ValidateToken 验证JWT token +func ValidateToken(tokenString string, cfg *config.Config) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("无效的签名方法") + } + return []byte(cfg.JWT.Secret), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("无效的token") +} diff --git a/backend/utils/password.go b/backend/utils/password.go new file mode 100644 index 0000000..8fb7cd1 --- /dev/null +++ b/backend/utils/password.go @@ -0,0 +1,18 @@ +package utils + +import "golang.org/x/crypto/bcrypt" + +// HashPassword 对密码进行bcrypt哈希加密 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// CheckPasswordHash 验证密码是否匹配哈希值 +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git "a/docs/\347\231\273\345\275\225\346\263\250\345\206\214/JWT\350\256\244\350\257\201\345\222\214\351\252\214\350\257\201\344\270\255\351\227\264\344\273\266.md" "b/docs/\347\231\273\345\275\225\346\263\250\345\206\214/JWT\350\256\244\350\257\201\345\222\214\351\252\214\350\257\201\344\270\255\351\227\264\344\273\266.md" new file mode 100644 index 0000000..ef0ebde --- /dev/null +++ "b/docs/\347\231\273\345\275\225\346\263\250\345\206\214/JWT\350\256\244\350\257\201\345\222\214\351\252\214\350\257\201\344\270\255\351\227\264\344\273\266.md" @@ -0,0 +1,247 @@ +# JWT 认证 和 验证中间件 +## 概述 + +后端使用JWT (JSON Web Token) 进行身份认证。前端需要通过登录接口获取 `accessToken`,然后在后续需要认证的API请求中携带此token。 + +## 认证流程 + +1. 新用户通过注册接口创建账号(已有账号可跳过此步骤) +2. 前端调用登录接口获取 `accessToken` +3. 前端存储 `accessToken`(推荐使用 localStorage 或 sessionStorage) +4. 后续需要认证的API请求在请求头中携带 `Authorization: Bearer ` +5. 后端验证token,验证失败返回 401 Unauthorized + +## API接口 + +### 登录接口 + +**接口地址**: `POST /api/auth/login` + +**请求头**: +``` +Content-Type: application/json +``` + +**请求体**: +```json +{ + "username": "admin", + "password": "admin123" +} +``` + +**说明**: +- `username`: 支持用户名或邮箱地址 +- `password`: 用户密码 + +**成功响应** (200 OK): +```json +{ + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "user": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "nickname": "系统管理员", + "avatar": "", + "phone": "", + "status": 1, + "last_login": "2024-01-01T12:00:00Z", + "created_at": "2024-01-01T10:00:00Z", + "updated_at": "2024-01-01T12:00:00Z" + } +} +``` + +**错误响应**: + +1. 请求参数错误 (400 Bad Request): +```json +{ + "error": "请求参数错误" +} +``` + +2. 用户名或密码错误 (401 Unauthorized): +```json +{ + "error": "用户名或密码错误" +} +``` + +3. 账户已被禁用 (403 Forbidden): +```json +{ + "error": "账户已被禁用" +} +``` + +4. 服务器错误 (500 Internal Server Error): +```json +{ + "error": "生成token失败" +} +``` + +### 注册接口 + +**接口地址**: `POST /api/auth/register` + +**请求头**: +``` +Content-Type: application/json +``` + +**请求体**: +```json +{ + "username": "testuser", + "email": "test@example.com", + "password": "password123", + "nickname": "测试用户" +} +``` + +**说明**: +- `username`: 用户名,必填,唯一标识 +- `email`: 邮箱地址,必填,必须是有效的邮箱格式,唯一标识 +- `password`: 密码,必填,至少6位字符 +- `nickname`: 昵称,可选 + +**成功响应** (201 Created): +```json +{ + "user": { + "id": 2, + "username": "testuser", + "email": "test@example.com", + "nickname": "测试用户", + "avatar": "", + "phone": "", + "status": 1, + "last_login": null, + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z" + } +} +``` + +**错误响应**: + +1. 请求参数错误 (400 Bad Request): +```json +{ + "error": "请求参数错误" +} +``` + +2. 密码格式不正确 (400 Bad Request): +```json +{ + "error": "密码格式不正确" +} +``` + +3. 用户名或邮箱已存在 (409 Conflict): +```json +{ + "error": "用户名或邮箱已存在" +} +``` + +4. 服务器错误 (500 Internal Server Error): +```json +{ + "error": "创建用户失败" +} +``` + +**注意**: 注册成功后,用户需要调用登录接口获取 `accessToken` 才能进行后续的认证操作。 + +## Token使用 + +### 在请求中使用Token + +对于需要认证的API请求,必须在请求头中添加 `Authorization` 字段: + +**请求头格式**: +``` +Authorization: Bearer +``` + +**说明**: +- `` 为登录接口返回的 `accessToken` 值 +- 格式必须为 `Bearer `,Bearer 和 token 之间有一个空格 + +### 后端中间件使用 + +后端使用 `AuthMiddleware` 中间件来验证token。验证成功后,中间件会将用户信息存储到请求上下文中,后端路由处理函数可以通过以下方式获取: + +**Gin框架获取方式**: +```go +userID := c.GetUint("user_id") // 获取用户ID (uint类型) +username := c.GetString("username") // 获取用户名 (string类型) +``` + +**说明**: +- `user_id`: 当前登录用户的ID(uint类型) +- `username`: 当前登录用户的用户名(string类型) +- 这些信息在token验证成功后自动设置到请求上下文 +- 在受保护的路由中,可以直接使用这些信息进行业务处理 + +## 错误处理 + +### 401 Unauthorized 响应 + +当token验证失败时,后端会返回 401 状态码,可能的原因和响应: + +1. **未提供token**: +```json +{ + "error": "未提供认证token" +} +``` + +2. **Token格式错误**: +```json +{ + "error": "认证token格式错误" +} +``` + +3. **Token无效或过期**: +```json +{ + "error": "无效的token" +} +``` + +## Token过期 + +JWT token具有过期时间,默认配置为24小时(可通过环境变量 `JWT_EXPIRE_HOURS` 配置)。 + +当token过期时,后端会返回 401 Unauthorized 状态码,前端需要引导用户重新登录。 + +## 注意事项 + +1. **Token安全**: + - 不要在URL参数中传递token + - 不要将token存储在可被XSS攻击访问的地方(如全局变量) + +2. **Token存储**: + - `localStorage` / `sessionStorage` + - 根据安全需求选择存储方式 + +3. **错误处理**: + - 统一处理401错误,自动跳转登录 + +4. **Token刷新**: + - 当前未实现token刷新机制 + - token过期后需要用户重新登录 + +## 测试账号 / 初始账号(在初始化数据库时会自动创建) + +- **用户名**: `admin` +- **密码**: `admin123` +- **邮箱**: `admin@example.com` + diff --git "a/docs/\347\263\273\347\273\237\345\210\235\345\247\213\345\214\226.md" "b/docs/\347\263\273\347\273\237\345\210\235\345\247\213\345\214\226.md" new file mode 100644 index 0000000..a891daa --- /dev/null +++ "b/docs/\347\263\273\347\273\237\345\210\235\345\247\213\345\214\226.md" @@ -0,0 +1,6 @@ +## 测试账号 / 初始账号(在初始化数据库时会自动创建) +在 database/seed.go中实现了系统管理员账户的初始化,具体来说,如果数据库中不存在`admin`账号,则会自动创建,账号密码如下: + +- **用户名**: `admin` +- **密码**: `admin123` +- **邮箱**: `admin@example.com` \ No newline at end of file From dcb1c0345bf9f10a30f0cb5c5d0e92282d639615 Mon Sep 17 00:00:00 2001 From: LinXin Date: Mon, 3 Nov 2025 20:12:21 +0800 Subject: [PATCH 2/3] feat: basic user profile api(for testing auth middleware) --- backend/handlers/user/profile.go | 48 ++++++++++++++++++++++++++++++++ backend/routes/routes.go | 5 ++-- backend/services/user_service.go | 38 +++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 backend/handlers/user/profile.go create mode 100644 backend/services/user_service.go diff --git a/backend/handlers/user/profile.go b/backend/handlers/user/profile.go new file mode 100644 index 0000000..9650f7f --- /dev/null +++ b/backend/handlers/user/profile.go @@ -0,0 +1,48 @@ +package user + +import ( + "net/http" + "progress-wall-backend/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ProfileHandler 用户信息处理器 +type ProfileHandler struct { + userService *services.UserService +} + +// NewProfileHandler 创建用户信息处理器 +func NewProfileHandler(db *gorm.DB) *ProfileHandler { + return &ProfileHandler{ + userService: services.NewUserService(db), + } +} + +// GetProfile 获取当前登录用户信息 +func (h *ProfileHandler) GetProfile(c *gin.Context) { + // 从中间件设置的上下文中获取用户ID + userID := c.GetUint("user_id") + if userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"}) + return + } + + // 调用service层获取用户信息 + user, err := h.userService.GetUserByID(userID) + if err != nil { + switch err { + case services.ErrUserNotFound: + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + + // 返回用户信息 + c.JSON(http.StatusOK, gin.H{ + "user": user, + }) +} diff --git a/backend/routes/routes.go b/backend/routes/routes.go index bbb14dc..fdd9939 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -3,6 +3,7 @@ package routes import ( "progress-wall-backend/config" "progress-wall-backend/handlers/auth" + "progress-wall-backend/handlers/user" "progress-wall-backend/middleware" "strings" @@ -31,6 +32,7 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { // 初始化处理器 loginHandler := auth.NewLoginHandler(db, cfg) registerHandler := auth.NewRegisterHandler(db, cfg) + profileHandler := user.NewProfileHandler(db) // 公开路由(不需要认证) api := r.Group("/api") @@ -46,8 +48,7 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { protected := api.Group("") protected.Use(middleware.AuthMiddleware(cfg)) { - // 这里可以添加需要认证的路由 - // 例如: protected.GET("/profile", userHandler.GetProfile) + protected.GET("/user/profile", profileHandler.GetProfile) } return r diff --git a/backend/services/user_service.go b/backend/services/user_service.go new file mode 100644 index 0000000..077cee6 --- /dev/null +++ b/backend/services/user_service.go @@ -0,0 +1,38 @@ +package services + +import ( + "errors" + "progress-wall-backend/models" + + "gorm.io/gorm" +) + +// UserService 用户服务 +type UserService struct { + db *gorm.DB +} + +// NewUserService 创建用户服务 +func NewUserService(db *gorm.DB) *UserService { + return &UserService{ + db: db, + } +} + +// GetUserByID 根据ID获取用户信息 +func (s *UserService) GetUserByID(userID uint) (*models.User, error) { + var user models.User + result := s.db.First(&user, userID) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, errors.New("查询用户失败") + } + + // 清除敏感信息 + user.Password = "" + + return &user, nil +} From 3818768c602619ec64c49bc0439a913500c017f0 Mon Sep 17 00:00:00 2001 From: LinXin Date: Wed, 5 Nov 2025 19:25:42 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20problem=20suggested=20by=20deepseek?= =?UTF-8?q?=201.=20seed.go=20=E5=87=BD=E6=95=B0=E5=91=BD=E5=90=8D=E5=92=8C?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=20=E2=9C=85=20=E5=B0=86=20createDefaultUsers?= =?UTF-8?q?=20=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA=20createAdminUser=20?= =?UTF-8?q?=E2=9C=85=20=E6=9B=B4=E6=96=B0=E5=87=BD=E6=95=B0=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=EF=BC=8C=E8=AF=B4=E6=98=8E=E5=8A=9F=E8=83=BD=E5=92=8C?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=202.=20=E7=94=A8=E6=88=B7=E5=AD=98=E5=9C=A8=E6=80=A7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E4=BC=98=E5=8C=96=20=E2=9C=85=20=E5=B0=86=20First=20?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E6=94=B9=E4=B8=BA=20Count=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=EF=BC=8C=E6=9B=B4=E5=81=A5=E5=A3=AE=20=E2=9C=85=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=203.=20=E5=AF=86=E7=A0=81=E5=8A=A0=E5=AF=86=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=80=A7=20=E2=9C=85=20=E5=B0=86=20bcrypt=20=E6=88=90=E6=9C=AC?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E4=BB=8E=E9=BB=98=E8=AE=A4=E5=80=BC=2010=20?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E5=88=B0=2012=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=80=A7=20=E2=9C=85=20=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E5=8A=A0=E5=AF=86=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=B7=B2?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=EF=BC=88=E6=97=A0=E9=9C=80=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=EF=BC=89=204.=20=E6=95=8F=E6=84=9F=E4=BF=A1=E6=81=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=20=E2=9C=85=20=E5=B7=B2=E7=A1=AE=E8=AE=A4=E5=9C=A8?= =?UTF-8?q?=E4=BB=A5=E4=B8=8B=E4=BD=8D=E7=BD=AE=E6=B8=85=E9=99=A4=E4=BA=86?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E5=AD=97=E6=AE=B5=EF=BC=9A=20auth=5Fservice.?= =?UTF-8?q?go=20=E7=9A=84=20Login=20=E6=96=B9=E6=B3=95=EF=BC=88=E7=AC=AC77?= =?UTF-8?q?=E8=A1=8C=EF=BC=89=20auth=5Fservice.go=20=E7=9A=84=20Register?= =?UTF-8?q?=20=E6=96=B9=E6=B3=95=EF=BC=88=E7=AC=AC144=E8=A1=8C=EF=BC=89=20?= =?UTF-8?q?user=5Fservice.go=20=E7=9A=84=20GetUserByID=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=EF=BC=88=E7=AC=AC35=E8=A1=8C=EF=BC=89=205.=20?= =?UTF-8?q?=E5=85=B3=E4=BA=8E=20Preload=20=E4=BC=98=E5=8C=96=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=20=E5=9C=A8=20auth=5Fservice.go=20=E7=9A=84=20Login?= =?UTF-8?q?=20=E6=96=B9=E6=B3=95=E4=B8=AD=EF=BC=8C=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E4=B8=8D=E9=9C=80=E8=A6=81=20Preload?= =?UTF-8?q?=EF=BC=8C=E5=9B=A0=E4=B8=BA=EF=BC=9A=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=97=B6=E5=8F=AA=E9=9C=80=E9=AA=8C=E8=AF=81=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E5=92=8C=E5=AF=86=E7=A0=81=20=E4=B8=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E5=8A=A0=E8=BD=BD=E5=85=B3=E8=81=94=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=EF=BC=88=E6=9D=83=E9=99=90=E3=80=81=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=AD=89=EF=BC=89=20=E6=B7=BB=E5=8A=A0=20Preload=20=E4=BC=9A?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=BC=80=E9=94=80=20=E5=A6=82=E6=9E=9C=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E5=8A=A0=E8=BD=BD=E7=94=A8=E6=88=B7=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=88=96=E5=85=B6=E4=BB=96=E5=85=B3=E8=81=94=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E4=BB=A5=E5=9C=A8=E5=90=8E=E7=BB=AD=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=B8=AD=E5=86=8D=E6=B7=BB=E5=8A=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database/seed.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/database/seed.go b/backend/database/seed.go index 7ad49d1..7764006 100644 --- a/backend/database/seed.go +++ b/backend/database/seed.go @@ -13,8 +13,8 @@ import ( func Seed(db *gorm.DB) error { log.Println("开始初始化基础数据...") - // 创建默认用户 - if err := createDefaultUsers(db); err != nil { + // 创建默认管理员用户 + if err := createAdminUser(db); err != nil { return err } @@ -27,15 +27,18 @@ func Seed(db *gorm.DB) error { return nil } -// createDefaultUsers 创建默认用户 -// 该函数负责在系统初始化时创建默认的管理员用户 +// createAdminUser 创建默认管理员用户 +// 该函数负责在系统初始化时创建默认的管理员用户(username: admin, password: admin123) +// 如果管理员用户已存在,则跳过创建 // 参数: db - 数据库连接实例 // 返回: error - 如果创建失败则返回错误,否则返回nil -func createDefaultUsers(db *gorm.DB) error { +func createAdminUser(db *gorm.DB) error { // 检查admin用户是否已存在 - var existingUser models.User - result := db.Where("username = ?", "admin").First(&existingUser) - if result.Error == nil { + var count int64 + if err := db.Model(&models.User{}).Where("username = ?", "admin").Count(&count).Error; err != nil { + return fmt.Errorf("检查admin用户是否存在时出错: %w", err) + } + if count > 0 { log.Println("admin用户已存在,跳过创建") return nil }