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
@@ -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
}
+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
}