| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620 |
- 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 + "用户"
- }
- user := &entity.DtUser{
- Uid: generateUid(),
- 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,
- })
- }
|