304 lines
7.9 KiB
Go
304 lines
7.9 KiB
Go
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
|
||
}
|
||
}
|
||
|
||
// 超级管理员仅一个:第一个注册用户为超级管理员,后续均为普通用户
|
||
count, _ := coll.CountDocuments(ctx, bson.M{})
|
||
roleID := models.RoleIDUser
|
||
role := "user"
|
||
if count == 0 {
|
||
roleID = models.RoleIDSuperAdmin
|
||
role = "admin"
|
||
}
|
||
|
||
doc := bson.M{
|
||
"username": username,
|
||
"mobile": input.Mobile,
|
||
"password": utils.HashPassword(input.Password),
|
||
"role": role,
|
||
"role_id": roleID,
|
||
}
|
||
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
|
||
}
|