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 }