feat(auth+db): 实现注册登录核心逻辑,以及自动建表,用户信息落库
This commit is contained in:
@@ -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