auth.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. package daytask
  2. import (
  3. "app/apis/middleware"
  4. "app/commons/core/redisclient"
  5. "app/commons/model/entity"
  6. "crypto/md5"
  7. "crypto/rand"
  8. "encoding/hex"
  9. "fmt"
  10. "io"
  11. "math/big"
  12. "net/http"
  13. "net/url"
  14. "regexp"
  15. "strings"
  16. "time"
  17. "github.com/gin-gonic/gin"
  18. "golang.org/x/crypto/bcrypt"
  19. )
  20. // generateInviteCode 生成邀请码
  21. func generateInviteCode() string {
  22. const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  23. code := make([]byte, 8)
  24. for i := range code {
  25. n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
  26. code[i] = charset[n.Int64()]
  27. }
  28. return string(code)
  29. }
  30. // generateUid 生成用户UID
  31. func generateUid() string {
  32. return fmt.Sprintf("DT%d", time.Now().UnixNano()/1000000)
  33. }
  34. // sendSmsBao 调用短信宝发送短信
  35. func sendSmsBao(user, pass, sign, phone, code string) error {
  36. // 短信宝API接口
  37. // 国内: http://api.smsbao.com/sms
  38. // 国际: http://api.smsbao.com/wsms
  39. apiUrl := "http://api.smsbao.com/wsms" // 默认使用国际接口
  40. // 国内手机号使用国内接口
  41. if strings.HasPrefix(phone, "86") || (!strings.HasPrefix(phone, "+") && len(phone) == 11) {
  42. apiUrl = "http://api.smsbao.com/sms"
  43. // 国内手机号去掉区号前缀
  44. if strings.HasPrefix(phone, "86") {
  45. phone = phone[2:]
  46. }
  47. }
  48. // MD5加密密码
  49. h := md5.New()
  50. h.Write([]byte(pass))
  51. passHash := hex.EncodeToString(h.Sum(nil))
  52. // 短信内容
  53. content := fmt.Sprintf("【%s】您的验证码是%s,在5分钟内有效。", sign, code)
  54. // 构建请求参数
  55. params := url.Values{}
  56. params.Set("u", user)
  57. params.Set("p", passHash)
  58. params.Set("m", phone)
  59. params.Set("c", content)
  60. // 发送请求
  61. resp, err := http.Get(apiUrl + "?" + params.Encode())
  62. if err != nil {
  63. return fmt.Errorf("sms request failed: %v", err)
  64. }
  65. defer resp.Body.Close()
  66. body, _ := io.ReadAll(resp.Body)
  67. result := strings.TrimSpace(string(body))
  68. // 短信宝返回码: 0=成功, 其他=失败
  69. if result != "0" {
  70. return fmt.Errorf("smsbao error: %s", result)
  71. }
  72. return nil
  73. }
  74. // SendSmsCode 发送短信验证码
  75. func (s *Server) SendSmsCode(c *gin.Context) {
  76. ctx := s.FromContext(c)
  77. db := s.DB()
  78. type Req struct {
  79. Type string `json:"type" binding:"required"` // phone/email
  80. Account string `json:"account" binding:"required"` // 手机号或邮箱
  81. AreaCode string `json:"areaCode"` // 国际区号 如 84, 86
  82. Scene string `json:"scene" binding:"required"` // register/login/reset/bind
  83. }
  84. var req Req
  85. if err := c.ShouldBindJSON(&req); err != nil {
  86. ctx.Fail("invalid_params")
  87. return
  88. }
  89. // 目前只支持手机号
  90. if req.Type != "phone" {
  91. ctx.Fail("unsupported_type")
  92. return
  93. }
  94. // 验证手机号格式
  95. phoneRegex := regexp.MustCompile(`^\d{9,15}$`)
  96. if !phoneRegex.MatchString(req.Account) {
  97. ctx.Fail("invalid_phone")
  98. return
  99. }
  100. // 完整手机号(带区号)
  101. fullPhone := req.Account
  102. if req.AreaCode != "" {
  103. fullPhone = req.AreaCode + req.Account
  104. }
  105. // 防止重复发送
  106. cacheKey := fmt.Sprintf("sms:%s:%s", req.Scene, fullPhone)
  107. if !ctx.RepeatFilter(cacheKey, 60*time.Second) {
  108. ctx.Fail("sms_send_too_fast")
  109. return
  110. }
  111. // 生成验证码
  112. n, _ := rand.Int(rand.Reader, big.NewInt(900000))
  113. code := fmt.Sprintf("%06d", n.Int64()+100000)
  114. // 获取短信宝配置
  115. var smsUser, smsPass, smsSign string
  116. var config entity.DtConfig
  117. if err := db.Where("`key` = ?", entity.ConfigKeySmsUser).First(&config).Error; err == nil {
  118. smsUser = config.Value
  119. }
  120. if err := db.Where("`key` = ?", entity.ConfigKeySmsPass).First(&config).Error; err == nil {
  121. smsPass = config.Value
  122. }
  123. if err := db.Where("`key` = ?", entity.ConfigKeySmsSign).First(&config).Error; err == nil {
  124. smsSign = config.Value
  125. }
  126. // 调用短信宝发送验证码
  127. if smsUser != "" && smsPass != "" {
  128. if err := sendSmsBao(smsUser, smsPass, smsSign, fullPhone, code); err != nil {
  129. ctx.Fail("sms_send_failed")
  130. return
  131. }
  132. } else {
  133. // 开发模式:打印验证码到日志
  134. fmt.Printf("[DEV] SMS Code for %s: %s\n", fullPhone, code)
  135. }
  136. // 存储验证码到Redis
  137. codeKey := fmt.Sprintf("smscode:%s:%s", req.Scene, fullPhone)
  138. redisclient.DefaultClient().Set(c, codeKey, code, 5*time.Minute)
  139. ctx.OK(gin.H{
  140. "message": "sms_sent",
  141. })
  142. }
  143. // Register 用户注册
  144. func (s *Server) Register(c *gin.Context) {
  145. ctx := s.FromContext(c)
  146. db := s.DB()
  147. type Req struct {
  148. Type string `json:"type" binding:"required"` // phone/email
  149. Account string `json:"account" binding:"required"` // 手机号或邮箱
  150. Code string `json:"code" binding:"required"`
  151. Password string `json:"password" binding:"required,min=6"`
  152. InviteCode string `json:"inviteCode"`
  153. AreaCode string `json:"areaCode"` // 国际区号
  154. }
  155. var req Req
  156. if err := c.ShouldBindJSON(&req); err != nil {
  157. ctx.Fail("invalid_params")
  158. return
  159. }
  160. // 目前只支持手机号注册
  161. if req.Type != "phone" {
  162. ctx.Fail("unsupported_type")
  163. return
  164. }
  165. // 完整手机号(带区号)
  166. fullPhone := req.Account
  167. if req.AreaCode != "" {
  168. fullPhone = req.AreaCode + req.Account
  169. }
  170. // 验证短信验证码
  171. codeKey := fmt.Sprintf("smscode:register:%s", fullPhone)
  172. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  173. if err != nil || storedCode != req.Code {
  174. ctx.Fail("invalid_code")
  175. return
  176. }
  177. // 检查手机号是否已注册
  178. var existUser entity.DtUser
  179. if err := db.Where("phone = ?", fullPhone).First(&existUser).Error; err == nil {
  180. ctx.Fail("phone_registered")
  181. return
  182. }
  183. // 查找邀请人
  184. var parentId int64 = 0
  185. if req.InviteCode != "" {
  186. var inviter entity.DtUser
  187. if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil {
  188. parentId = inviter.Id
  189. }
  190. }
  191. // 加密密码
  192. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
  193. if err != nil {
  194. ctx.Fail("register_failed")
  195. return
  196. }
  197. // 获取默认等级
  198. var defaultLevel entity.DtUserLevel
  199. db.Where("is_default = ?", 1).First(&defaultLevel)
  200. // 创建用户
  201. user := &entity.DtUser{
  202. Uid: generateUid(),
  203. Phone: fullPhone,
  204. Password: string(hashedPassword),
  205. Nickname: "用户" + req.Account[len(req.Account)-4:],
  206. ParentId: parentId,
  207. LevelId: defaultLevel.Id,
  208. InviteCode: generateInviteCode(),
  209. Status: 1,
  210. }
  211. tx := db.Begin()
  212. if err := tx.Create(user).Error; err != nil {
  213. tx.Rollback()
  214. ctx.Fail("register_failed")
  215. return
  216. }
  217. // 更新邀请人的直推人数和团队人数
  218. if parentId > 0 {
  219. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  220. Updates(map[string]interface{}{
  221. "direct_invite_count": db.Raw("direct_invite_count + 1"),
  222. "team_count": db.Raw("team_count + 1"),
  223. })
  224. // 更新上级的团队人数(多级)
  225. var parent entity.DtUser
  226. if err := tx.Where("id = ?", parentId).First(&parent).Error; err == nil && parent.ParentId > 0 {
  227. tx.Model(&entity.DtUser{}).Where("id = ?", parent.ParentId).
  228. Update("team_count", db.Raw("team_count + 1"))
  229. }
  230. }
  231. tx.Commit()
  232. // 删除验证码
  233. redisclient.DefaultClient().Del(c, codeKey)
  234. // 生成Token
  235. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  236. if err != nil {
  237. ctx.Fail("register_failed")
  238. return
  239. }
  240. ctx.OK(gin.H{
  241. "token": token,
  242. "user": gin.H{
  243. "id": user.Id,
  244. "uid": user.Uid,
  245. "nickname": user.Nickname,
  246. "avatar": user.Avatar,
  247. "phone": user.Phone,
  248. },
  249. })
  250. }
  251. // LoginByPassword 密码登录
  252. func (s *Server) LoginByPassword(c *gin.Context) {
  253. ctx := s.FromContext(c)
  254. db := s.DB()
  255. type Req struct {
  256. Type string `json:"type" binding:"required"` // phone/email
  257. Account string `json:"account" binding:"required"` // 手机号或邮箱
  258. Password string `json:"password" binding:"required"`
  259. AreaCode string `json:"areaCode"` // 国际区号
  260. }
  261. var req Req
  262. if err := c.ShouldBindJSON(&req); err != nil {
  263. ctx.Fail("invalid_params")
  264. return
  265. }
  266. // 完整手机号(带区号)
  267. fullPhone := req.Account
  268. if req.AreaCode != "" {
  269. fullPhone = req.AreaCode + req.Account
  270. }
  271. // 查找用户
  272. var user entity.DtUser
  273. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  274. ctx.Fail("user_not_found")
  275. return
  276. }
  277. // 检查用户状态
  278. if user.Status != 1 {
  279. ctx.Fail("user_disabled")
  280. return
  281. }
  282. // 验证密码
  283. if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
  284. ctx.Fail("invalid_password")
  285. return
  286. }
  287. // 更新登录时间
  288. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  289. Update("last_login_at", time.Now().Unix())
  290. // 生成Token
  291. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  292. if err != nil {
  293. ctx.Fail("login_failed")
  294. return
  295. }
  296. ctx.OK(gin.H{
  297. "token": token,
  298. "user": gin.H{
  299. "id": user.Id,
  300. "uid": user.Uid,
  301. "nickname": user.Nickname,
  302. "avatar": user.Avatar,
  303. "phone": user.Phone,
  304. },
  305. })
  306. }
  307. // LoginBySms 短信验证码登录
  308. func (s *Server) LoginBySms(c *gin.Context) {
  309. ctx := s.FromContext(c)
  310. db := s.DB()
  311. type Req struct {
  312. Phone string `json:"phone" binding:"required"`
  313. Code string `json:"code" binding:"required"`
  314. AreaCode string `json:"areaCode"` // 国际区号
  315. }
  316. var req Req
  317. if err := c.ShouldBindJSON(&req); err != nil {
  318. ctx.Fail("invalid_params")
  319. return
  320. }
  321. // 完整手机号(带区号)
  322. fullPhone := req.Phone
  323. if req.AreaCode != "" {
  324. fullPhone = req.AreaCode + req.Phone
  325. }
  326. // 验证短信验证码
  327. codeKey := fmt.Sprintf("smscode:login:%s", fullPhone)
  328. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  329. if err != nil || storedCode != req.Code {
  330. ctx.Fail("invalid_code")
  331. return
  332. }
  333. // 查找用户
  334. var user entity.DtUser
  335. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  336. ctx.Fail("user_not_found")
  337. return
  338. }
  339. // 检查用户状态
  340. if user.Status != 1 {
  341. ctx.Fail("user_disabled")
  342. return
  343. }
  344. // 更新登录时间
  345. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  346. Update("last_login_at", time.Now().Unix())
  347. // 删除验证码
  348. redisclient.DefaultClient().Del(c, codeKey)
  349. // 生成Token
  350. token, err2 := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  351. if err2 != nil {
  352. ctx.Fail("login_failed")
  353. return
  354. }
  355. ctx.OK(gin.H{
  356. "token": token,
  357. "user": gin.H{
  358. "id": user.Id,
  359. "uid": user.Uid,
  360. "nickname": user.Nickname,
  361. "avatar": user.Avatar,
  362. "phone": user.Phone,
  363. },
  364. })
  365. }
  366. // ResetPassword 重置密码
  367. func (s *Server) ResetPassword(c *gin.Context) {
  368. ctx := s.FromContext(c)
  369. db := s.DB()
  370. type Req struct {
  371. Type string `json:"type" binding:"required"` // phone/email
  372. Account string `json:"account" binding:"required"` // 手机号或邮箱
  373. Code string `json:"code" binding:"required"`
  374. NewPassword string `json:"newPassword" binding:"required,min=6"`
  375. AreaCode string `json:"areaCode"` // 国际区号
  376. }
  377. var req Req
  378. if err := c.ShouldBindJSON(&req); err != nil {
  379. ctx.Fail("invalid_params")
  380. return
  381. }
  382. // 完整手机号(带区号)
  383. fullPhone := req.Account
  384. if req.AreaCode != "" {
  385. fullPhone = req.AreaCode + req.Account
  386. }
  387. // 验证短信验证码
  388. codeKey := fmt.Sprintf("smscode:reset:%s", fullPhone)
  389. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  390. if err != nil || storedCode != req.Code {
  391. ctx.Fail("invalid_code")
  392. return
  393. }
  394. // 查找用户
  395. var user entity.DtUser
  396. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  397. ctx.Fail("user_not_found")
  398. return
  399. }
  400. // 加密新密码
  401. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
  402. if err != nil {
  403. ctx.Fail("reset_failed")
  404. return
  405. }
  406. // 更新密码
  407. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  408. Update("password", string(hashedPassword))
  409. // 删除验证码
  410. redisclient.DefaultClient().Del(c, codeKey)
  411. ctx.OK(gin.H{
  412. "message": "password_reset_success",
  413. })
  414. }
  415. // OAuthLogin OAuth登录(Google/Zalo等)
  416. func (s *Server) OAuthLogin(c *gin.Context) {
  417. ctx := s.FromContext(c)
  418. db := s.DB()
  419. type Req struct {
  420. Provider string `json:"provider" binding:"required"` // google/zalo/telegram
  421. OpenId string `json:"openId" binding:"required"`
  422. Nickname string `json:"nickname"`
  423. Avatar string `json:"avatar"`
  424. InviteCode string `json:"inviteCode"`
  425. }
  426. var req Req
  427. if err := c.ShouldBindJSON(&req); err != nil {
  428. ctx.Fail("invalid_params")
  429. return
  430. }
  431. // 查找是否已绑定
  432. var social entity.DtUserSocial
  433. if err := db.Where("platform = ? AND account = ?", req.Provider, req.OpenId).First(&social).Error; err == nil {
  434. // 已绑定,直接登录
  435. var user entity.DtUser
  436. if err := db.Where("id = ?", social.UserId).First(&user).Error; err != nil {
  437. ctx.Fail("user_not_found")
  438. return
  439. }
  440. if user.Status != 1 {
  441. ctx.Fail("user_disabled")
  442. return
  443. }
  444. // 更新登录时间
  445. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  446. Update("last_login_at", time.Now().Unix())
  447. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  448. if err != nil {
  449. ctx.Fail("login_failed")
  450. return
  451. }
  452. ctx.OK(gin.H{
  453. "token": token,
  454. "user": gin.H{
  455. "id": user.Id,
  456. "uid": user.Uid,
  457. "nickname": user.Nickname,
  458. "avatar": user.Avatar,
  459. "phone": user.Phone,
  460. },
  461. })
  462. return
  463. }
  464. // 未绑定,创建新用户
  465. var parentId int64 = 0
  466. if req.InviteCode != "" {
  467. var inviter entity.DtUser
  468. if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil {
  469. parentId = inviter.Id
  470. }
  471. }
  472. // 获取默认等级
  473. var defaultLevel entity.DtUserLevel
  474. db.Where("is_default = ?", 1).First(&defaultLevel)
  475. nickname := req.Nickname
  476. if nickname == "" {
  477. nickname = req.Provider + "用户"
  478. }
  479. user := &entity.DtUser{
  480. Uid: generateUid(),
  481. Nickname: nickname,
  482. Avatar: req.Avatar,
  483. ParentId: parentId,
  484. LevelId: defaultLevel.Id,
  485. InviteCode: generateInviteCode(),
  486. Status: 1,
  487. }
  488. tx := db.Begin()
  489. if err := tx.Create(user).Error; err != nil {
  490. tx.Rollback()
  491. ctx.Fail("login_failed")
  492. return
  493. }
  494. // 创建社交账号绑定
  495. social = entity.DtUserSocial{
  496. UserId: user.Id,
  497. Platform: req.Provider,
  498. Account: req.OpenId,
  499. Nickname: req.Nickname,
  500. Avatar: req.Avatar,
  501. }
  502. if err := tx.Create(&social).Error; err != nil {
  503. tx.Rollback()
  504. ctx.Fail("login_failed")
  505. return
  506. }
  507. // 更新邀请人统计
  508. if parentId > 0 {
  509. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  510. Updates(map[string]interface{}{
  511. "direct_invite_count": db.Raw("direct_invite_count + 1"),
  512. "team_count": db.Raw("team_count + 1"),
  513. })
  514. }
  515. tx.Commit()
  516. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  517. if err != nil {
  518. ctx.Fail("login_failed")
  519. return
  520. }
  521. ctx.OK(gin.H{
  522. "token": token,
  523. "user": gin.H{
  524. "id": user.Id,
  525. "uid": user.Uid,
  526. "nickname": user.Nickname,
  527. "avatar": user.Avatar,
  528. "phone": user.Phone,
  529. },
  530. "isNew": true,
  531. })
  532. }