Procházet zdrojové kódy

feat: 手机+短信验证码登陆

runningwater před 8 měsíci
rodič
revize
7d8d36e351

+ 37 - 0
app/http/controllers/api/v1/auth/login_controller.go

@@ -0,0 +1,37 @@
+package auth
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "github.com/runningwater/gohub/app/http/controllers/api/v1"
+	"github.com/runningwater/gohub/app/requests"
+	"github.com/runningwater/gohub/pkg/auth"
+	"github.com/runningwater/gohub/pkg/jwt"
+	"github.com/runningwater/gohub/pkg/response"
+)
+
+type LoginController struct {
+	v1.BaseApiController
+}
+
+// LoginByPhone 手机号登录
+func (lc *LoginController) LoginByPhone(c *gin.Context) {
+	// 1. 验证表单
+	request := requests.LoginByPhoneRequest{}
+	if ok := requests.Validate(c, &request, requests.LoginByPhone); !ok {
+		return
+	}
+
+	// 2. 尝试登录
+	user, err := auth.LoginByPhone(request.Phone)
+	if err != nil {
+		// 失败,显示错误提示
+		response.Error(c, err, "账号不存在或密码错误")
+	}
+	// 登录成功
+	token := jwt.NewJWT().IssueToken(user.GetStringID(), user.Name)
+
+	response.JSON(c, gin.H{
+		"token": token,
+	})
+
+}

+ 40 - 40
app/http/controllers/api/v1/auth/verify_code_controller.go

@@ -1,64 +1,64 @@
 package auth
 
 import (
-    "github.com/gin-gonic/gin"
+	"github.com/gin-gonic/gin"
 
-    v1 "github.com/runningwater/gohub/app/http/controllers/api/v1"
-    "github.com/runningwater/gohub/app/requests"
-    "github.com/runningwater/gohub/pkg/captcha"
-    "github.com/runningwater/gohub/pkg/logger"
-    "github.com/runningwater/gohub/pkg/response"
-    "github.com/runningwater/gohub/pkg/verifycode"
+	v1 "github.com/runningwater/gohub/app/http/controllers/api/v1"
+	"github.com/runningwater/gohub/app/requests"
+	"github.com/runningwater/gohub/pkg/captcha"
+	"github.com/runningwater/gohub/pkg/logger"
+	"github.com/runningwater/gohub/pkg/response"
+	"github.com/runningwater/gohub/pkg/verifycode"
 )
 
 type VerifyCodeController struct {
-    v1.BaseApiController
+	v1.BaseApiController
 }
 
 // ShowCaptcha 显示图片验证码
 func (vc *VerifyCodeController) ShowCaptcha(c *gin.Context) {
-    // 生成验证码
-    id, b64s, answer, err := captcha.NewCaptcha().GenerateCaptcha()
-    logger.LogIf(err)
-    logger.DebugString("captchaController", "captcha id: ", id)
-    logger.DebugString("captchaController", "captcha answer: ", answer)
-    response.JSON(c, gin.H{
-        "captcha_id":    id,
-        "captcha_image": b64s,
-    })
+	// 生成验证码
+	id, b64s, answer, err := captcha.NewCaptcha().GenerateCaptcha()
+	logger.LogIf(err)
+	logger.DebugString("captchaController", "captcha id: ", id)
+	logger.DebugString("captchaController", "captcha answer: ", answer)
+	response.JSON(c, gin.H{
+		"captcha_id":    id,
+		"captcha_image": b64s,
+	})
 }
 
 // SendUsingPhone 发送手机验证码
 func (vc *VerifyCodeController) SendUsingPhone(c *gin.Context) {
 
-    // 1. 验证表单
-    request := requests.VerifyCodeRequest{}
-    if ok := requests.Validate(c, &request, requests.VerifyCodePhone); !ok {
-        return
-    }
+	// 1. 验证表单
+	request := requests.VerifyCodeRequest{}
+	if ok := requests.Validate(c, &request, requests.VerifyCodePhone); !ok {
+		return
+	}
 
-    // 2. 发送手机验证码
-    if ok := verifycode.NewVerifyCode().SendSMS(request.Phone); !ok {
-        response.Abort500(c, "发送短信失败~~")
-    } else {
-        response.Success(c)
-    }
+	// 2. 发送手机验证码
+	if ok := verifycode.NewVerifyCode().SendSMS(request.Phone); !ok {
+		response.Abort500(c, "发送短信失败~~")
+	} else {
+		response.Success(c)
+	}
 
-    response.Success(c)
+	response.Success(c)
 }
 
 // SendUsingEmail 发送邮箱验证码
 func (vc *VerifyCodeController) SendUsingEmail(c *gin.Context) {
-    // 1. 验证表单
-    request := requests.VerifyCodeEmailRequest{}
-    if ok := requests.Validate(c, &request, requests.VerifyCodeEmail); !ok {
-        return
-    }
+	// 1. 验证表单
+	request := requests.VerifyCodeEmailRequest{}
+	if ok := requests.Validate(c, &request, requests.VerifyCodeEmail); !ok {
+		return
+	}
 
-    // 2. 发送邮箱验证码
-    if err := verifycode.NewVerifyCode().SendEmail(request.Email); err != nil {
-        response.Abort500(c, "发送邮件失败~~")
-    } else {
-        response.Success(c)
-    }
+	// 2. 发送邮箱验证码
+	if err := verifycode.NewVerifyCode().SendEmail(request.Email); err != nil {
+		response.Abort500(c, "发送邮件失败~~")
+	} else {
+		response.Success(c)
+	}
 }

+ 8 - 1
app/models/model.go

@@ -1,6 +1,9 @@
 package models
 
-import "time"
+import (
+	"fmt"
+	"time"
+)
 
 // BaseModel 基础模型
 type BaseModel struct {
@@ -11,3 +14,7 @@ type CommonTimestampsField struct {
 	CreatedAt time.Time `gorm:"column:created_at;index;" json:"created_at,omitempty"` // 创建时间
 	UpdatedAt time.Time `gorm:"column:updated_at;index;" json:"updated_at,omitempty"` // 更新时间
 }
+
+func GetStringID(id uint) string {
+	return fmt.Sprintf("%d", id)
+}

+ 4 - 0
app/models/user/user_model.go

@@ -24,6 +24,10 @@ func (u *User) Create() {
 	database.DB.Create(&u)
 }
 
+func (u *User) GetStringID() string {
+	return models.GetStringID(u.ID)
+}
+
 // ComparePassword 密码是否匹配
 func (u *User) ComparePassword(_password string) bool {
 	return hash.BcryptCheck(_password, u.Password)

+ 18 - 0
app/models/user/user_util.go

@@ -13,3 +13,21 @@ func IsPhoneExist(phone string) bool {
 	database.DB.Model(&User{}).Where("phone = ?", phone).Count(&count)
 	return count > 0
 }
+
+// GetByMulti 多条件查询,支持手机号、邮箱和用户名
+// 按顺序检查并返回第一个匹配的用户
+func GetByMulti(loginID string) (userModel User) {
+	database.DB.
+		Where("phone = ?", loginID).
+		Or("email =?", loginID).
+		Or("name =?", loginID).
+		First(&userModel)
+
+	return
+}
+
+// GetByPhone 根据手机号查询用户
+func GetByPhone(phone string) (userModel User) {
+	database.DB.Where("phone = ?", phone).First(&userModel)
+	return
+}

+ 42 - 0
app/requests/login_request.go

@@ -0,0 +1,42 @@
+package requests
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/runningwater/gohub/app/requests/validators"
+	"github.com/thedevsaddam/govalidator"
+)
+
+type LoginByPhoneRequest struct {
+	Phone      string `json:"phone,omitempty" valid:"phone"`
+	VerifyCode string `json:"verify_code,omitempty" valid:"verify_code"`
+}
+
+// LoginByPhone 验证表单,返回长度等于零即通过
+func LoginByPhone(data any, c *gin.Context) map[string][]string {
+	// 1. 定制认证规则
+	rules := govalidator.MapData{
+		"phone":       []string{"required", "digits:11"},
+		"verify_code": []string{"required", "digits:6"},
+	}
+	// 2. 定制错误消息
+	messages := govalidator.MapData{
+		"phone": []string{
+			"required:手机号为必填项,参数名称 phone",
+			"digits:手机号长度必须为 11 位的数字",
+		},
+		"verify_code": []string{
+			"required:验证码答案必填",
+			"digits:验证码长度必须为 6 位的数字",
+		},
+	}
+
+	errs := validate(data, rules, messages)
+
+	// 3. ...  增加其他业务逻辑
+	_data := data.(*LoginByPhoneRequest)
+	errs = validators.ValidateVerifyCode(_data.Phone, _data.VerifyCode, errs)
+
+	// 4. 返回错误消息
+	return errs
+
+}

+ 32 - 0
pkg/auth/auth.go

@@ -0,0 +1,32 @@
+// Package auth provides authentication and authorization functionality.
+package auth
+
+import (
+	"errors"
+
+	"github.com/runningwater/gohub/app/models/user"
+)
+
+// Attemp login
+func Attemp(email, password string) (user.User, error) {
+	userModel := user.GetByMulti(email)
+	if userModel.ID == 0 {
+		return user.User{}, errors.New("账号不存在")
+	}
+
+	if !userModel.ComparePassword(password) {
+		return user.User{}, errors.New("密码错误")
+	}
+
+	return userModel, nil
+}
+
+// LoginByPhone 登陆指定用户
+func LoginByPhone(phone string) (user.User, error) {
+	userModel := user.GetByPhone(phone)
+	if userModel.ID == 0 {
+		return user.User{}, errors.New("账号不存在")
+	}
+
+	return userModel, nil
+}

+ 4 - 0
routes/api.go

@@ -29,6 +29,10 @@ func RegisterAPIRoutes(router *gin.Engine) {
 			authGroup.POST("/verify_code/phone", vcc.SendUsingPhone)
 			// 发送邮箱验证码
 			authGroup.POST("/verify_code/email", vcc.SendUsingEmail)
+
+			logc := new(auth.LoginController)
+			// 手机号登录
+			authGroup.POST("/login/using-phone", logc.LoginByPhone)
 		}
 	}
 }