Files
web/server/handlers/register.go
2026-03-17 01:00:11 +08:00

295 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"context"
"errors"
"log"
"net/http"
"regexp"
"sync"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"yh_web/server/config"
"yh_web/server/models"
"yh_web/server/utils"
"github.com/gin-gonic/gin"
)
const (
testVerifyCode = "8888" // 测试验证码(未接入短信时使用)
codeExpire = 5 * time.Minute // 验证码有效期
)
var (
codeStore = make(map[string]codeEntry)
codeStoreMu sync.RWMutex
)
type codeEntry struct {
Code string
ExpireAt time.Time
}
// SendCodeInput 发送验证码请求
type SendCodeInput struct {
Mobile string `json:"mobile" binding:"required"`
}
// RegisterInput 手机注册请求
type RegisterInput struct {
Mobile string `json:"mobile" binding:"required"`
Code string `json:"code" binding:"required"`
Password string `json:"password" binding:"required"`
Username string `json:"username"` // 可选,默认用手机号
Email string `json:"email"` // 可选
}
// SendCode 发送验证码(测试阶段:任意手机号输入 8888 即可)
func SendCode(c *gin.Context) {
var input SendCodeInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入手机号"})
return
}
if !isValidMobile(input.Mobile) {
c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
return
}
// 未接入短信平台,使用测试验证码 8888
codeStoreMu.Lock()
codeStore[input.Mobile] = codeEntry{
Code: testVerifyCode,
ExpireAt: time.Now().Add(codeExpire),
}
codeStoreMu.Unlock()
c.JSON(http.StatusOK, gin.H{
"message": "验证码已发送(测试环境请输入 8888",
"expire": int(codeExpire.Seconds()),
})
}
// Register 手机号注册
func Register(c *gin.Context) {
var input RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写手机号、验证码和密码"})
return
}
if !isValidMobile(input.Mobile) {
c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
return
}
if len(input.Password) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "密码至少6位"})
return
}
if input.Email != "" && !isValidEmail(input.Email) {
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
return
}
// 校验验证码
codeStoreMu.RLock()
entry, ok := codeStore[input.Mobile]
codeStoreMu.RUnlock()
if !ok || entry.Code != input.Code {
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
return
}
if time.Now().After(entry.ExpireAt) {
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码已过期,请重新获取"})
return
}
// 删除已用验证码
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
username := input.Username
if username == "" {
username = input.Mobile
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("users")
// 检查手机号是否已注册
var exist models.User
err := coll.FindOne(ctx, bson.M{"mobile": input.Mobile}).Decode(&exist)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "该手机号已注册"})
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
log.Printf("[Register] FindOne mobile error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
err = coll.FindOne(ctx, bson.M{"username": username}).Decode(&exist)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
log.Printf("[Register] FindOne username error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
// 若提供了邮箱,检查是否已注册
if input.Email != "" {
err = coll.FindOne(ctx, bson.M{"email": input.Email}).Decode(&exist)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "该邮箱已注册"})
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
log.Printf("[Register] FindOne email error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
}
doc := bson.M{
"username": username,
"mobile": input.Mobile,
"password": utils.HashPassword(input.Password),
"role": "admin",
"role_id": models.RoleIDSuperAdmin,
}
if input.Email != "" {
doc["email"] = input.Email
}
_, err = coll.InsertOne(ctx, doc)
if err != nil {
log.Printf("[Register] InsertOne error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "注册成功",
})
}
// ResetPasswordInput 密码找回请求
type ResetPasswordInput struct {
Mobile string `json:"mobile" binding:"required"`
Code string `json:"code" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
// ResetPassword 密码找回(手机号+验证码)
func ResetPassword(c *gin.Context) {
var input ResetPasswordInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写手机号、验证码和新密码"})
return
}
if !isValidMobile(input.Mobile) {
c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
return
}
if len(input.NewPassword) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "新密码至少6位"})
return
}
// 校验验证码
codeStoreMu.RLock()
entry, ok := codeStore[input.Mobile]
codeStoreMu.RUnlock()
if !ok || entry.Code != input.Code {
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
return
}
if time.Now().After(entry.ExpireAt) {
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码已过期,请重新获取"})
return
}
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("users")
var user models.User
err := coll.FindOne(ctx, bson.M{"mobile": input.Mobile}).Decode(&user)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
c.JSON(http.StatusNotFound, gin.H{"error": "该手机号未注册"})
return
}
log.Printf("[ResetPassword] FindOne error: %v", err)
resp := gin.H{"error": "操作失败"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
_, err = coll.UpdateOne(ctx, bson.M{"_id": user.ID}, bson.M{"$set": bson.M{"password": utils.HashPassword(input.NewPassword)}})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "密码已重置,请登录"})
}
func isValidMobile(mobile string) bool {
// 简单校验11位数字1开头
matched, _ := regexp.MatchString(`^1\d{10}$`, mobile)
return matched
}
func isValidEmail(email string) bool {
// 简单邮箱格式校验
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
return matched
}