feat(auth+db): 实现注册登录核心逻辑,以及自动建表,用户信息落库

This commit is contained in:
2026-05-24 13:53:03 +08:00
parent afb33d4d5d
commit 7c0ce6fce8
9 changed files with 198 additions and 23 deletions
+21 -1
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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=
+20
View File
@@ -11,6 +11,9 @@ type Config struct {
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
}
+20
View File
@@ -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...)
}
+15 -10
View File
@@ -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,
}))
}
+19 -6
View File
@@ -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 -4
View File
@@ -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"`
}
+79
View File
@@ -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
}