|
@@ -4,13 +4,20 @@ import (
|
|
|
"app/apis/middleware"
|
|
"app/apis/middleware"
|
|
|
"app/commons/core/redisclient"
|
|
"app/commons/core/redisclient"
|
|
|
"app/commons/model/entity"
|
|
"app/commons/model/entity"
|
|
|
|
|
+ "crypto/md5"
|
|
|
"crypto/rand"
|
|
"crypto/rand"
|
|
|
|
|
+ "encoding/hex"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
- "github.com/gin-gonic/gin"
|
|
|
|
|
- "golang.org/x/crypto/bcrypt"
|
|
|
|
|
|
|
+ "io"
|
|
|
"math/big"
|
|
"math/big"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "net/url"
|
|
|
"regexp"
|
|
"regexp"
|
|
|
|
|
+ "strings"
|
|
|
"time"
|
|
"time"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
|
|
+ "golang.org/x/crypto/bcrypt"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// generateInviteCode 生成邀请码
|
|
// generateInviteCode 生成邀请码
|
|
@@ -29,13 +36,65 @@ func generateUid() string {
|
|
|
return fmt.Sprintf("DT%d", time.Now().UnixNano()/1000000)
|
|
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 发送短信验证码
|
|
// SendSmsCode 发送短信验证码
|
|
|
func (s *Server) SendSmsCode(c *gin.Context) {
|
|
func (s *Server) SendSmsCode(c *gin.Context) {
|
|
|
ctx := s.FromContext(c)
|
|
ctx := s.FromContext(c)
|
|
|
|
|
+ db := s.DB()
|
|
|
|
|
|
|
|
type Req struct {
|
|
type Req struct {
|
|
|
- Phone string `json:"phone" binding:"required"`
|
|
|
|
|
- Type string `json:"type" binding:"required"` // register/login/reset
|
|
|
|
|
|
|
+ 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
|
|
var req Req
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
@@ -43,15 +102,27 @@ func (s *Server) SendSmsCode(c *gin.Context) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 目前只支持手机号
|
|
|
|
|
+ if req.Type != "phone" {
|
|
|
|
|
+ ctx.Fail("unsupported_type")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 验证手机号格式
|
|
// 验证手机号格式
|
|
|
phoneRegex := regexp.MustCompile(`^\d{9,15}$`)
|
|
phoneRegex := regexp.MustCompile(`^\d{9,15}$`)
|
|
|
- if !phoneRegex.MatchString(req.Phone) {
|
|
|
|
|
|
|
+ if !phoneRegex.MatchString(req.Account) {
|
|
|
ctx.Fail("invalid_phone")
|
|
ctx.Fail("invalid_phone")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 完整手机号(带区号)
|
|
|
|
|
+ fullPhone := req.Account
|
|
|
|
|
+ if req.AreaCode != "" {
|
|
|
|
|
+ fullPhone = req.AreaCode + req.Account
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 防止重复发送
|
|
// 防止重复发送
|
|
|
- cacheKey := fmt.Sprintf("sms:%s:%s", req.Type, req.Phone)
|
|
|
|
|
|
|
+ cacheKey := fmt.Sprintf("sms:%s:%s", req.Scene, fullPhone)
|
|
|
if !ctx.RepeatFilter(cacheKey, 60*time.Second) {
|
|
if !ctx.RepeatFilter(cacheKey, 60*time.Second) {
|
|
|
ctx.Fail("sms_send_too_fast")
|
|
ctx.Fail("sms_send_too_fast")
|
|
|
return
|
|
return
|
|
@@ -61,11 +132,32 @@ func (s *Server) SendSmsCode(c *gin.Context) {
|
|
|
n, _ := rand.Int(rand.Reader, big.NewInt(900000))
|
|
n, _ := rand.Int(rand.Reader, big.NewInt(900000))
|
|
|
code := fmt.Sprintf("%06d", n.Int64()+100000)
|
|
code := fmt.Sprintf("%06d", n.Int64()+100000)
|
|
|
|
|
|
|
|
- // TODO: 调用短信服务发送验证码
|
|
|
|
|
- // smsService.Send(req.Phone, code)
|
|
|
|
|
|
|
+ // 获取短信宝配置
|
|
|
|
|
+ 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
|
|
// 存储验证码到Redis
|
|
|
- codeKey := fmt.Sprintf("smscode:%s:%s", req.Type, req.Phone)
|
|
|
|
|
|
|
+ codeKey := fmt.Sprintf("smscode:%s:%s", req.Scene, fullPhone)
|
|
|
redisclient.DefaultClient().Set(c, codeKey, code, 5*time.Minute)
|
|
redisclient.DefaultClient().Set(c, codeKey, code, 5*time.Minute)
|
|
|
|
|
|
|
|
ctx.OK(gin.H{
|
|
ctx.OK(gin.H{
|
|
@@ -79,10 +171,12 @@ func (s *Server) Register(c *gin.Context) {
|
|
|
db := s.DB()
|
|
db := s.DB()
|
|
|
|
|
|
|
|
type Req struct {
|
|
type Req struct {
|
|
|
- Phone string `json:"phone" binding:"required"`
|
|
|
|
|
|
|
+ Type string `json:"type" binding:"required"` // phone/email
|
|
|
|
|
+ Account string `json:"account" binding:"required"` // 手机号或邮箱
|
|
|
Code string `json:"code" binding:"required"`
|
|
Code string `json:"code" binding:"required"`
|
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
|
InviteCode string `json:"inviteCode"`
|
|
InviteCode string `json:"inviteCode"`
|
|
|
|
|
+ AreaCode string `json:"areaCode"` // 国际区号
|
|
|
}
|
|
}
|
|
|
var req Req
|
|
var req Req
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
@@ -90,8 +184,20 @@ func (s *Server) Register(c *gin.Context) {
|
|
|
return
|
|
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", req.Phone)
|
|
|
|
|
|
|
+ codeKey := fmt.Sprintf("smscode:register:%s", fullPhone)
|
|
|
storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
|
|
storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
|
|
|
if err != nil || storedCode != req.Code {
|
|
if err != nil || storedCode != req.Code {
|
|
|
ctx.Fail("invalid_code")
|
|
ctx.Fail("invalid_code")
|
|
@@ -100,7 +206,7 @@ func (s *Server) Register(c *gin.Context) {
|
|
|
|
|
|
|
|
// 检查手机号是否已注册
|
|
// 检查手机号是否已注册
|
|
|
var existUser entity.DtUser
|
|
var existUser entity.DtUser
|
|
|
- if err := db.Where("phone = ?", req.Phone).First(&existUser).Error; err == nil {
|
|
|
|
|
|
|
+ if err := db.Where("phone = ?", fullPhone).First(&existUser).Error; err == nil {
|
|
|
ctx.Fail("phone_registered")
|
|
ctx.Fail("phone_registered")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -128,9 +234,9 @@ func (s *Server) Register(c *gin.Context) {
|
|
|
// 创建用户
|
|
// 创建用户
|
|
|
user := &entity.DtUser{
|
|
user := &entity.DtUser{
|
|
|
Uid: generateUid(),
|
|
Uid: generateUid(),
|
|
|
- Phone: req.Phone,
|
|
|
|
|
|
|
+ Phone: fullPhone,
|
|
|
Password: string(hashedPassword),
|
|
Password: string(hashedPassword),
|
|
|
- Nickname: "用户" + req.Phone[len(req.Phone)-4:],
|
|
|
|
|
|
|
+ Nickname: "用户" + req.Account[len(req.Account)-4:],
|
|
|
ParentId: parentId,
|
|
ParentId: parentId,
|
|
|
LevelId: defaultLevel.Id,
|
|
LevelId: defaultLevel.Id,
|
|
|
InviteCode: generateInviteCode(),
|
|
InviteCode: generateInviteCode(),
|
|
@@ -191,8 +297,10 @@ func (s *Server) LoginByPassword(c *gin.Context) {
|
|
|
db := s.DB()
|
|
db := s.DB()
|
|
|
|
|
|
|
|
type Req struct {
|
|
type Req struct {
|
|
|
- Phone string `json:"phone" binding:"required"`
|
|
|
|
|
|
|
+ Type string `json:"type" binding:"required"` // phone/email
|
|
|
|
|
+ Account string `json:"account" binding:"required"` // 手机号或邮箱
|
|
|
Password string `json:"password" binding:"required"`
|
|
Password string `json:"password" binding:"required"`
|
|
|
|
|
+ AreaCode string `json:"areaCode"` // 国际区号
|
|
|
}
|
|
}
|
|
|
var req Req
|
|
var req Req
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
@@ -200,9 +308,15 @@ func (s *Server) LoginByPassword(c *gin.Context) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 完整手机号(带区号)
|
|
|
|
|
+ fullPhone := req.Account
|
|
|
|
|
+ if req.AreaCode != "" {
|
|
|
|
|
+ fullPhone = req.AreaCode + req.Account
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 查找用户
|
|
// 查找用户
|
|
|
var user entity.DtUser
|
|
var user entity.DtUser
|
|
|
- if err := db.Where("phone = ?", req.Phone).First(&user).Error; err != nil {
|
|
|
|
|
|
|
+ if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
|
|
|
ctx.Fail("user_not_found")
|
|
ctx.Fail("user_not_found")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -248,8 +362,9 @@ func (s *Server) LoginBySms(c *gin.Context) {
|
|
|
db := s.DB()
|
|
db := s.DB()
|
|
|
|
|
|
|
|
type Req struct {
|
|
type Req struct {
|
|
|
- Phone string `json:"phone" binding:"required"`
|
|
|
|
|
- Code string `json:"code" binding:"required"`
|
|
|
|
|
|
|
+ Phone string `json:"phone" binding:"required"`
|
|
|
|
|
+ Code string `json:"code" binding:"required"`
|
|
|
|
|
+ AreaCode string `json:"areaCode"` // 国际区号
|
|
|
}
|
|
}
|
|
|
var req Req
|
|
var req Req
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
@@ -257,8 +372,14 @@ func (s *Server) LoginBySms(c *gin.Context) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 完整手机号(带区号)
|
|
|
|
|
+ fullPhone := req.Phone
|
|
|
|
|
+ if req.AreaCode != "" {
|
|
|
|
|
+ fullPhone = req.AreaCode + req.Phone
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 验证短信验证码
|
|
// 验证短信验证码
|
|
|
- codeKey := fmt.Sprintf("smscode:login:%s", req.Phone)
|
|
|
|
|
|
|
+ codeKey := fmt.Sprintf("smscode:login:%s", fullPhone)
|
|
|
storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
|
|
storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
|
|
|
if err != nil || storedCode != req.Code {
|
|
if err != nil || storedCode != req.Code {
|
|
|
ctx.Fail("invalid_code")
|
|
ctx.Fail("invalid_code")
|
|
@@ -267,7 +388,7 @@ func (s *Server) LoginBySms(c *gin.Context) {
|
|
|
|
|
|
|
|
// 查找用户
|
|
// 查找用户
|
|
|
var user entity.DtUser
|
|
var user entity.DtUser
|
|
|
- if err := db.Where("phone = ?", req.Phone).First(&user).Error; err != nil {
|
|
|
|
|
|
|
+ if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
|
|
|
ctx.Fail("user_not_found")
|
|
ctx.Fail("user_not_found")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -286,8 +407,8 @@ func (s *Server) LoginBySms(c *gin.Context) {
|
|
|
redisclient.DefaultClient().Del(c, codeKey)
|
|
redisclient.DefaultClient().Del(c, codeKey)
|
|
|
|
|
|
|
|
// 生成Token
|
|
// 生成Token
|
|
|
- token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
|
|
|
|
|
- if err != nil {
|
|
|
|
|
|
|
+ token, err2 := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
|
|
|
|
|
+ if err2 != nil {
|
|
|
ctx.Fail("login_failed")
|
|
ctx.Fail("login_failed")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -310,9 +431,11 @@ func (s *Server) ResetPassword(c *gin.Context) {
|
|
|
db := s.DB()
|
|
db := s.DB()
|
|
|
|
|
|
|
|
type Req struct {
|
|
type Req struct {
|
|
|
- Phone string `json:"phone" binding:"required"`
|
|
|
|
|
|
|
+ Type string `json:"type" binding:"required"` // phone/email
|
|
|
|
|
+ Account string `json:"account" binding:"required"` // 手机号或邮箱
|
|
|
Code string `json:"code" binding:"required"`
|
|
Code string `json:"code" binding:"required"`
|
|
|
NewPassword string `json:"newPassword" binding:"required,min=6"`
|
|
NewPassword string `json:"newPassword" binding:"required,min=6"`
|
|
|
|
|
+ AreaCode string `json:"areaCode"` // 国际区号
|
|
|
}
|
|
}
|
|
|
var req Req
|
|
var req Req
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
@@ -320,8 +443,14 @@ func (s *Server) ResetPassword(c *gin.Context) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 完整手机号(带区号)
|
|
|
|
|
+ fullPhone := req.Account
|
|
|
|
|
+ if req.AreaCode != "" {
|
|
|
|
|
+ fullPhone = req.AreaCode + req.Account
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 验证短信验证码
|
|
// 验证短信验证码
|
|
|
- codeKey := fmt.Sprintf("smscode:reset:%s", req.Phone)
|
|
|
|
|
|
|
+ codeKey := fmt.Sprintf("smscode:reset:%s", fullPhone)
|
|
|
storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
|
|
storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
|
|
|
if err != nil || storedCode != req.Code {
|
|
if err != nil || storedCode != req.Code {
|
|
|
ctx.Fail("invalid_code")
|
|
ctx.Fail("invalid_code")
|
|
@@ -330,7 +459,7 @@ func (s *Server) ResetPassword(c *gin.Context) {
|
|
|
|
|
|
|
|
// 查找用户
|
|
// 查找用户
|
|
|
var user entity.DtUser
|
|
var user entity.DtUser
|
|
|
- if err := db.Where("phone = ?", req.Phone).First(&user).Error; err != nil {
|
|
|
|
|
|
|
+ if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
|
|
|
ctx.Fail("user_not_found")
|
|
ctx.Fail("user_not_found")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|