feat(auth+db): 实现注册登录核心逻辑,以及自动建表,用户信息落库
This commit is contained in:
+21
-1
@@ -6,7 +6,9 @@ import (
|
||||
"log"
|
||||
|
||||
"gen2d/internal/config"
|
||||
"gen2d/internal/db"
|
||||
"gen2d/internal/handler"
|
||||
"gen2d/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -16,11 +18,29 @@ func main() {
|
||||
|
||||
gin.SetMode(cfg.Mode)
|
||||
|
||||
// 初始化 SQLite 数据库
|
||||
if err := db.Init(cfg.DSN, &model.User{}); err != nil {
|
||||
log.Fatalf("db init failed: %v", err)
|
||||
}
|
||||
|
||||
// 初始化 AuthService,注入 JWT 配置
|
||||
handler.InitAuthService(cfg.JWTSecret, cfg.JWTExpire)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery()) // panic 恢复中间件,防止服务因未捕获异常宕机
|
||||
|
||||
// API v1 路由组
|
||||
r.GET("/api/v1/health", handler.Health) // 健康检查
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/health", handler.Health) // 健康检查
|
||||
}
|
||||
|
||||
// Auth 路由组
|
||||
auth := r.Group("/auth")
|
||||
{
|
||||
auth.POST("/register", handler.Register) // 用户注册
|
||||
auth.POST("/login", handler.Login) // 用户登录
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf(":%d", cfg.Port)
|
||||
log.Printf("gen2d backend starting on %s", addr)
|
||||
|
||||
+7
-1
@@ -5,6 +5,9 @@ go 1.26.3
|
||||
require (
|
||||
github.com/cloudwego/eino v0.8.13
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -23,13 +26,17 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/goph/emperror v0.17.2 // 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/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
||||
@@ -45,7 +52,6 @@ require (
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
|
||||
@@ -48,6 +48,8 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -59,6 +61,10 @@ github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
@@ -83,6 +89,8 @@ github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -179,3 +187,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
|
||||
@@ -8,9 +8,12 @@ import (
|
||||
|
||||
// Config 应用全局配置,优先读取环境变量,未设置时使用默认值。
|
||||
type Config struct {
|
||||
Port int // HTTP 监听端口,默认 8080,环境变量 GEN2D_PORT
|
||||
Port int // HTTP 监听端口,默认 8080,环境变量 GEN2D_PORT
|
||||
Mode string // Gin 运行模式 (debug/release),环境变量 GEN2D_MODE
|
||||
MaxFileSize int64 // 上传文件大小上限(字节),默认 10MB
|
||||
DSN string // SQLite 数据库路径,默认 data/gen2d.db,环境变量 GEN2D_DSN
|
||||
JWTSecret string // JWT 签名密钥,环境变量 GEN2D_JWT_SECRET
|
||||
JWTExpire int64 // JWT 过期时间(秒),默认 7200
|
||||
}
|
||||
|
||||
// Load 从环境变量加载配置并返回。
|
||||
@@ -19,6 +22,9 @@ func Load() *Config {
|
||||
Port: 8080,
|
||||
Mode: "debug",
|
||||
MaxFileSize: 10 << 20, // 10MB
|
||||
DSN: "data/gen2d.db",
|
||||
JWTSecret: "gen2d-dev-secret",
|
||||
JWTExpire: 7200,
|
||||
}
|
||||
|
||||
if port := os.Getenv("GEN2D_PORT"); port != "" {
|
||||
@@ -31,5 +37,19 @@ func Load() *Config {
|
||||
cfg.Mode = mode
|
||||
}
|
||||
|
||||
if dsn := os.Getenv("GEN2D_DSN"); dsn != "" {
|
||||
cfg.DSN = dsn
|
||||
}
|
||||
|
||||
if secret := os.Getenv("GEN2D_JWT_SECRET"); secret != "" {
|
||||
cfg.JWTSecret = secret
|
||||
}
|
||||
|
||||
if expire := os.Getenv("GEN2D_JWT_EXPIRE"); expire != "" {
|
||||
if e, err := strconv.ParseInt(expire, 10, 64); err == nil {
|
||||
cfg.JWTExpire = e
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DB 全局数据库实例,Init 后可用。
|
||||
var DB *gorm.DB
|
||||
|
||||
// Init 初始化 SQLite 数据库连接并执行自动迁移。
|
||||
func Init(dsn string, models ...any) error {
|
||||
var err error
|
||||
DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return DB.AutoMigrate(models...)
|
||||
}
|
||||
@@ -10,18 +10,18 @@ import (
|
||||
|
||||
// LoginRequest 登录请求参数。
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"` // 用户名
|
||||
Password string `json:"password" binding:"required"` // 密码
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录成功返回的凭证及用户信息。
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"` // 访问令牌
|
||||
ExpiresIn int64 `json:"expiresIn"` // 过期时间(秒)
|
||||
User model.User `json:"user"` // 用户信息
|
||||
Token string `json:"token"`
|
||||
ExpiresIn int64 `json:"expiresIn"`
|
||||
User model.User `json:"user"`
|
||||
}
|
||||
|
||||
// Login 用户登录接口,校验用户名/密码并返回访问令牌。
|
||||
// Login 用户登录接口,校验用户名/密码并返回 JWT 令牌。
|
||||
func Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -29,10 +29,15 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 调用 service 层校验用户凭证并签发 token
|
||||
token, expiresIn, user, err := authSvc.Login(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.OK(LoginResponse{
|
||||
Token: "placeholder-token",
|
||||
ExpiresIn: 7200,
|
||||
User: model.User{Username: req.Username},
|
||||
Token: token,
|
||||
ExpiresIn: expiresIn,
|
||||
User: *user,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4,15 +4,23 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gen2d/internal/model"
|
||||
"gen2d/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var authSvc *service.AuthService
|
||||
|
||||
// InitAuthService 由 main 在启动时调用,注入 JWT 配置。
|
||||
func InitAuthService(jwtSecret string, jwtExpire int64) {
|
||||
authSvc = service.NewAuthService(jwtSecret, jwtExpire)
|
||||
}
|
||||
|
||||
// RegisterRequest 注册请求参数。
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=32"` // 用户名
|
||||
Password string `json:"password" binding:"required,min=6,max=64"` // 密码
|
||||
Email string `json:"email" binding:"omitempty,email"` // 邮箱(可选)
|
||||
Username string `json:"username" binding:"required,min=3,max=32"`
|
||||
Password string `json:"password" binding:"required,min=6,max=64"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
}
|
||||
|
||||
// RegisterResponse 注册成功返回的用户信息。
|
||||
@@ -29,9 +37,14 @@ func Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 调用 service 层创建用户
|
||||
user, err := authSvc.Register(c.Request.Context(), req.Username, req.Password, req.Email)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusConflict, model.Fail(http.StatusConflict, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, model.OK(RegisterResponse{
|
||||
ID: 0,
|
||||
Username: req.Username,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import "time"
|
||||
|
||||
// User 用户实体。
|
||||
type User struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"` // 密码哈希,JSON 序列化时忽略
|
||||
Email string `json:"email,omitempty"`
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Username string `json:"username" gorm:"uniqueIndex;size:32"`
|
||||
Password string `json:"-"`
|
||||
Email string `json:"email,omitempty" gorm:"size:128"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gen2d/internal/db"
|
||||
"gen2d/internal/model"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthService 用户认证服务。
|
||||
type AuthService struct {
|
||||
jwtSecret []byte
|
||||
jwtExpire time.Duration
|
||||
}
|
||||
|
||||
// NewAuthService 创建 AuthService 实例。
|
||||
func NewAuthService(jwtSecret string, jwtExpire int64) *AuthService {
|
||||
return &AuthService{
|
||||
jwtSecret: []byte(jwtSecret),
|
||||
jwtExpire: time.Duration(jwtExpire) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册新用户,密码使用 bcrypt 加密存储。
|
||||
func (s *AuthService) Register(ctx context.Context, username, password, email string) (*model.User, error) {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: username,
|
||||
Password: string(hashed),
|
||||
Email: email,
|
||||
}
|
||||
|
||||
if err := db.DB.WithContext(ctx).Create(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return nil, errors.New("用户名已存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Login 校验用户名密码,成功返回 JWT token 及用户信息。
|
||||
func (s *AuthService) Login(ctx context.Context, username, password string) (string, int64, *model.User, error) {
|
||||
var user model.User
|
||||
if err := db.DB.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", 0, nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return "", 0, nil, err
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return "", 0, nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(s.jwtExpire)
|
||||
claims := jwt.MapClaims{
|
||||
"sub": user.ID,
|
||||
"exp": expiresAt.Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.jwtSecret)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
|
||||
return token, int64(s.jwtExpire.Seconds()), &user, nil
|
||||
}
|
||||
Reference in New Issue
Block a user