package daytask import ( "app/apis/middleware" "app/commons/core/redisclient" "app/commons/model/entity" "crypto/md5" "crypto/rand" "encoding/hex" "fmt" "io" "math/big" "net/http" "net/url" "regexp" "strings" "time" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" ) // generateInviteCode 生成邀请码 func generateInviteCode() string { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" code := make([]byte, 8) for i := range code { n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) code[i] = charset[n.Int64()] } return string(code) } // generateUid 生成用户UID func generateUid() string { return fmt.Sprintf("DT%d", time.Now().UnixNano()/1000000) } // sendSmsBao 调用短信宝发送短信 func sendSmsBao(user, pass, sign, phone, code string) error { // 短信宝API接口 // 国内: http://api.smsbao.com/sms // 国际: http://api.smsbao.com/wsms apiUrl := "http://api.smsbao.com/wsms" // 默认使用国际接口 // 国内手机号使用国内接口 if strings.HasPrefix(phone, "86") || (!strings.HasPrefix(phone, "+") && len(phone) == 11) { apiUrl = "http://api.smsbao.com/sms" // 国内手机号去掉区号前缀 if strings.HasPrefix(phone, "86") { phone = phone[2:] } } // MD5加密密码 h := md5.New() h.Write([]byte(pass)) passHash := hex.EncodeToString(h.Sum(nil)) // 短信内容 content := fmt.Sprintf("【%s】您的验证码是%s,在5分钟内有效。", sign, code) // 构建请求参数 params := url.Values{} params.Set("u", user) params.Set("p", passHash) params.Set("m", phone) params.Set("c", content) // 发送请求 resp, err := http.Get(apiUrl + "?" + params.Encode()) if err != nil { return fmt.Errorf("sms request failed: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) result := strings.TrimSpace(string(body)) // 短信宝返回码: 0=成功, 其他=失败 if result != "0" { return fmt.Errorf("smsbao error: %s", result) } return nil } // SendSmsCode 发送短信验证码 func (s *Server) SendSmsCode(c *gin.Context) { ctx := s.FromContext(c) db := s.DB() type Req struct { Type string `json:"type" binding:"required"` // phone/email Account string `json:"account" binding:"required"` // 手机号或邮箱 AreaCode string `json:"areaCode"` // 国际区号 如 84, 86 Scene string `json:"scene" binding:"required"` // register/login/reset/bind } var req Req if err := c.ShouldBindJSON(&req); err != nil { ctx.Fail("invalid_params") return } // 目前只支持手机号 if req.Type != "phone" { ctx.Fail("unsupported_type") return } // 验证手机号格式 phoneRegex := regexp.MustCompile(`^\d{9,15}$`) if !phoneRegex.MatchString(req.Account) { ctx.Fail("invalid_phone") return } // 完整手机号(带区号) fullPhone := req.Account if req.AreaCode != "" { fullPhone = req.AreaCode + req.Account } // 防止重复发送 cacheKey := fmt.Sprintf("sms:%s:%s", req.Scene, fullPhone) if !ctx.RepeatFilter(cacheKey, 60*time.Second) { ctx.Fail("sms_send_too_fast") return } // 生成验证码 n, _ := rand.Int(rand.Reader, big.NewInt(900000)) code := fmt.Sprintf("%06d", n.Int64()+100000) // 获取短信宝配置 var smsUser, smsPass, smsSign string var config entity.DtConfig if err := db.Where("`key` = ?", entity.ConfigKeySmsUser).First(&config).Error; err == nil { smsUser = config.Value } if err := db.Where("`key` = ?", entity.ConfigKeySmsPass).First(&config).Error; err == nil { smsPass = config.Value } if err := db.Where("`key` = ?", entity.ConfigKeySmsSign).First(&config).Error; err == nil { smsSign = config.Value } // 调用短信宝发送验证码 if smsUser != "" && smsPass != "" { if err := sendSmsBao(smsUser, smsPass, smsSign, fullPhone, code); err != nil { ctx.Fail("sms_send_failed") return } } else { // 开发模式:打印验证码到日志 fmt.Printf("[DEV] SMS Code for %s: %s\n", fullPhone, code) } // 存储验证码到Redis codeKey := fmt.Sprintf("smscode:%s:%s", req.Scene, fullPhone) redisclient.DefaultClient().Set(c, codeKey, code, 5*time.Minute) ctx.OK(gin.H{ "message": "sms_sent", }) } // Register 用户注册 func (s *Server) Register(c *gin.Context) { ctx := s.FromContext(c) db := s.DB() type Req struct { Type string `json:"type" binding:"required"` // phone/email Account string `json:"account" binding:"required"` // 手机号或邮箱 Code string `json:"code" binding:"required"` Password string `json:"password" binding:"required,min=6"` InviteCode string `json:"inviteCode"` AreaCode string `json:"areaCode"` // 国际区号 } var req Req if err := c.ShouldBindJSON(&req); err != nil { ctx.Fail("invalid_params") return } // 目前只支持手机号注册 if req.Type != "phone" { ctx.Fail("unsupported_type") return } // 完整手机号(带区号) fullPhone := req.Account if req.AreaCode != "" { fullPhone = req.AreaCode + req.Account } // 验证短信验证码 codeKey := fmt.Sprintf("smscode:register:%s", fullPhone) storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result() if err != nil || storedCode != req.Code { ctx.Fail("invalid_code") return } // 检查手机号是否已注册 var existUser entity.DtUser if err := db.Where("phone = ?", fullPhone).First(&existUser).Error; err == nil { ctx.Fail("phone_registered") return } // 查找邀请人 var parentId int64 = 0 if req.InviteCode != "" { var inviter entity.DtUser if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil { parentId = inviter.Id } } // 加密密码 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { ctx.Fail("register_failed") return } // 获取默认等级 var defaultLevel entity.DtUserLevel db.Where("is_default = ?", 1).First(&defaultLevel) // 创建用户 user := &entity.DtUser{ Uid: generateUid(), Phone: fullPhone, Password: string(hashedPassword), Nickname: "用户" + req.Account[len(req.Account)-4:], ParentId: parentId, LevelId: defaultLevel.Id, InviteCode: generateInviteCode(), Status: 1, } tx := db.Begin() if err := tx.Create(user).Error; err != nil { tx.Rollback() ctx.Fail("register_failed") return } // 更新邀请人的直推人数和团队人数 if parentId > 0 { tx.Model(&entity.DtUser{}).Where("id = ?", parentId). Updates(map[string]interface{}{ "direct_invite_count": db.Raw("direct_invite_count + 1"), "team_count": db.Raw("team_count + 1"), }) // 更新上级的团队人数(多级) var parent entity.DtUser if err := tx.Where("id = ?", parentId).First(&parent).Error; err == nil && parent.ParentId > 0 { tx.Model(&entity.DtUser{}).Where("id = ?", parent.ParentId). Update("team_count", db.Raw("team_count + 1")) } } tx.Commit() // 删除验证码 redisclient.DefaultClient().Del(c, codeKey) // 生成Token token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid}) if err != nil { ctx.Fail("register_failed") return } ctx.OK(gin.H{ "token": token, "user": gin.H{ "id": user.Id, "uid": user.Uid, "nickname": user.Nickname, "avatar": user.Avatar, "phone": user.Phone, }, }) } // LoginByPassword 密码登录 func (s *Server) LoginByPassword(c *gin.Context) { ctx := s.FromContext(c) db := s.DB() type Req struct { Type string `json:"type" binding:"required"` // phone/email Account string `json:"account" binding:"required"` // 手机号或邮箱 Password string `json:"password" binding:"required"` AreaCode string `json:"areaCode"` // 国际区号 } var req Req if err := c.ShouldBindJSON(&req); err != nil { ctx.Fail("invalid_params") return } // 完整手机号(带区号) fullPhone := req.Account if req.AreaCode != "" { fullPhone = req.AreaCode + req.Account } // 查找用户 var user entity.DtUser if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil { ctx.Fail("user_not_found") return } // 检查用户状态 if user.Status != 1 { ctx.Fail("user_disabled") return } // 验证密码 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { ctx.Fail("invalid_password") return } // 更新登录时间 db.Model(&entity.DtUser{}).Where("id = ?", user.Id). Update("last_login_at", time.Now().Unix()) // 生成Token token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid}) if err != nil { ctx.Fail("login_failed") return } ctx.OK(gin.H{ "token": token, "user": gin.H{ "id": user.Id, "uid": user.Uid, "nickname": user.Nickname, "avatar": user.Avatar, "phone": user.Phone, }, }) } // LoginBySms 短信验证码登录 func (s *Server) LoginBySms(c *gin.Context) { ctx := s.FromContext(c) db := s.DB() type Req struct { Phone string `json:"phone" binding:"required"` Code string `json:"code" binding:"required"` AreaCode string `json:"areaCode"` // 国际区号 } var req Req if err := c.ShouldBindJSON(&req); err != nil { ctx.Fail("invalid_params") return } // 完整手机号(带区号) fullPhone := req.Phone if req.AreaCode != "" { fullPhone = req.AreaCode + req.Phone } // 验证短信验证码 codeKey := fmt.Sprintf("smscode:login:%s", fullPhone) storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result() if err != nil || storedCode != req.Code { ctx.Fail("invalid_code") return } // 查找用户 var user entity.DtUser if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil { ctx.Fail("user_not_found") return } // 检查用户状态 if user.Status != 1 { ctx.Fail("user_disabled") return } // 更新登录时间 db.Model(&entity.DtUser{}).Where("id = ?", user.Id). Update("last_login_at", time.Now().Unix()) // 删除验证码 redisclient.DefaultClient().Del(c, codeKey) // 生成Token token, err2 := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid}) if err2 != nil { ctx.Fail("login_failed") return } ctx.OK(gin.H{ "token": token, "user": gin.H{ "id": user.Id, "uid": user.Uid, "nickname": user.Nickname, "avatar": user.Avatar, "phone": user.Phone, }, }) } // ResetPassword 重置密码 func (s *Server) ResetPassword(c *gin.Context) { ctx := s.FromContext(c) db := s.DB() type Req struct { Type string `json:"type" binding:"required"` // phone/email Account string `json:"account" binding:"required"` // 手机号或邮箱 Code string `json:"code" binding:"required"` NewPassword string `json:"newPassword" binding:"required,min=6"` AreaCode string `json:"areaCode"` // 国际区号 } var req Req if err := c.ShouldBindJSON(&req); err != nil { ctx.Fail("invalid_params") return } // 完整手机号(带区号) fullPhone := req.Account if req.AreaCode != "" { fullPhone = req.AreaCode + req.Account } // 验证短信验证码 codeKey := fmt.Sprintf("smscode:reset:%s", fullPhone) storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result() if err != nil || storedCode != req.Code { ctx.Fail("invalid_code") return } // 查找用户 var user entity.DtUser if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil { ctx.Fail("user_not_found") return } // 加密新密码 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { ctx.Fail("reset_failed") return } // 更新密码 db.Model(&entity.DtUser{}).Where("id = ?", user.Id). Update("password", string(hashedPassword)) // 删除验证码 redisclient.DefaultClient().Del(c, codeKey) ctx.OK(gin.H{ "message": "password_reset_success", }) } // OAuthLogin OAuth登录(Google/Zalo等) func (s *Server) OAuthLogin(c *gin.Context) { ctx := s.FromContext(c) db := s.DB() type Req struct { Provider string `json:"provider" binding:"required"` // google/zalo/telegram OpenId string `json:"openId" binding:"required"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` InviteCode string `json:"inviteCode"` } var req Req if err := c.ShouldBindJSON(&req); err != nil { ctx.Fail("invalid_params") return } // 查找是否已绑定 var social entity.DtUserSocial if err := db.Where("platform = ? AND account = ?", req.Provider, req.OpenId).First(&social).Error; err == nil { // 已绑定,直接登录 var user entity.DtUser if err := db.Where("id = ?", social.UserId).First(&user).Error; err != nil { ctx.Fail("user_not_found") return } if user.Status != 1 { ctx.Fail("user_disabled") return } // 更新登录时间 db.Model(&entity.DtUser{}).Where("id = ?", user.Id). Update("last_login_at", time.Now().Unix()) token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid}) if err != nil { ctx.Fail("login_failed") return } ctx.OK(gin.H{ "token": token, "user": gin.H{ "id": user.Id, "uid": user.Uid, "nickname": user.Nickname, "avatar": user.Avatar, "phone": user.Phone, }, }) return } // 未绑定,创建新用户 var parentId int64 = 0 if req.InviteCode != "" { var inviter entity.DtUser if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil { parentId = inviter.Id } } // 获取默认等级 var defaultLevel entity.DtUserLevel db.Where("is_default = ?", 1).First(&defaultLevel) nickname := req.Nickname if nickname == "" { nickname = req.Provider + "用户" } // 为OAuth用户生成唯一占位手机号(oauth_provider_openid前16位) oauthPhone := fmt.Sprintf("oauth_%s_%s", req.Provider, req.OpenId[:16]) user := &entity.DtUser{ Uid: generateUid(), Phone: oauthPhone, Nickname: nickname, Avatar: req.Avatar, ParentId: parentId, LevelId: defaultLevel.Id, InviteCode: generateInviteCode(), Status: 1, } tx := db.Begin() if err := tx.Create(user).Error; err != nil { tx.Rollback() ctx.Fail("login_failed") return } // 创建社交账号绑定 social = entity.DtUserSocial{ UserId: user.Id, Platform: req.Provider, Account: req.OpenId, Nickname: req.Nickname, Avatar: req.Avatar, } if err := tx.Create(&social).Error; err != nil { tx.Rollback() ctx.Fail("login_failed") return } // 更新邀请人统计 if parentId > 0 { tx.Model(&entity.DtUser{}).Where("id = ?", parentId). Updates(map[string]interface{}{ "direct_invite_count": db.Raw("direct_invite_count + 1"), "team_count": db.Raw("team_count + 1"), }) } tx.Commit() token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid}) if err != nil { ctx.Fail("login_failed") return } ctx.OK(gin.H{ "token": token, "user": gin.H{ "id": user.Id, "uid": user.Uid, "nickname": user.Nickname, "avatar": user.Avatar, "phone": user.Phone, }, "isNew": true, }) }