auth.go 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078
  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. "encoding/json"
  10. "fmt"
  11. "io"
  12. "math/big"
  13. "net/http"
  14. "net/smtp"
  15. "net/url"
  16. "regexp"
  17. "strconv"
  18. "strings"
  19. "time"
  20. "github.com/gin-gonic/gin"
  21. "golang.org/x/crypto/bcrypt"
  22. )
  23. // generateInviteCode 生成邀请码
  24. func generateInviteCode() string {
  25. const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  26. code := make([]byte, 8)
  27. for i := range code {
  28. n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
  29. code[i] = charset[n.Int64()]
  30. }
  31. return string(code)
  32. }
  33. // generateUid 生成用户UID(时间戳+随机数,防止并发碰撞)
  34. func generateUid() string {
  35. n, _ := rand.Int(rand.Reader, big.NewInt(9999))
  36. return fmt.Sprintf("DT%d%04d", time.Now().UnixNano()/1000000, n.Int64())
  37. }
  38. // sendSmsBao 调用短信宝发送短信
  39. func sendSmsBao(user, pass, sign, phone, code string) error {
  40. // 短信宝API接口
  41. // 国内: http://api.smsbao.com/sms
  42. // 国际: http://api.smsbao.com/wsms
  43. apiUrl := "http://api.smsbao.com/wsms" // 默认使用国际接口
  44. // 国内手机号使用国内接口
  45. if strings.HasPrefix(phone, "86") || (!strings.HasPrefix(phone, "+") && len(phone) == 11) {
  46. apiUrl = "http://api.smsbao.com/sms"
  47. // 国内手机号去掉区号前缀
  48. if strings.HasPrefix(phone, "86") {
  49. phone = phone[2:]
  50. }
  51. }
  52. // MD5加密密码
  53. h := md5.New()
  54. h.Write([]byte(pass))
  55. passHash := hex.EncodeToString(h.Sum(nil))
  56. // 短信内容
  57. content := fmt.Sprintf("【%s】您的验证码是%s,在5分钟内有效。", sign, code)
  58. // 构建请求参数
  59. params := url.Values{}
  60. params.Set("u", user)
  61. params.Set("p", passHash)
  62. params.Set("m", phone)
  63. params.Set("c", content)
  64. // 发送请求
  65. resp, err := http.Get(apiUrl + "?" + params.Encode())
  66. if err != nil {
  67. return fmt.Errorf("sms request failed: %v", err)
  68. }
  69. defer resp.Body.Close()
  70. body, _ := io.ReadAll(resp.Body)
  71. result := strings.TrimSpace(string(body))
  72. // 短信宝返回码: 0=成功, 其他=失败
  73. if result != "0" {
  74. return fmt.Errorf("smsbao error: %s", result)
  75. }
  76. return nil
  77. }
  78. // sendEmail 发送邮件验证码
  79. func sendEmail(host string, port int, user, pass, fromName, toEmail, code string) error {
  80. from := user
  81. subject := "Verification Code"
  82. body := fmt.Sprintf(`
  83. <div style="padding:20px;font-family:Arial,sans-serif;">
  84. <h2 style="color:#ffc300;">Vitiens</h2>
  85. <p>Your verification code is:</p>
  86. <h1 style="color:#ffc300;font-size:36px;letter-spacing:8px;">%s</h1>
  87. <p>This code will expire in 5 minutes.</p>
  88. <p style="color:#999;font-size:12px;">If you did not request this code, please ignore this email.</p>
  89. </div>
  90. `, code)
  91. // 构建邮件内容(固定header顺序)
  92. msg := fmt.Sprintf("From: %s <%s>\r\n", fromName, from)
  93. msg += fmt.Sprintf("To: %s\r\n", toEmail)
  94. msg += fmt.Sprintf("Subject: %s\r\n", subject)
  95. msg += "MIME-Version: 1.0\r\n"
  96. msg += "Content-Type: text/html; charset=UTF-8\r\n"
  97. msg += "\r\n" + body
  98. addr := fmt.Sprintf("%s:%d", host, port)
  99. auth := smtp.PlainAuth("", user, pass, host)
  100. return smtp.SendMail(addr, auth, from, []string{toEmail}, []byte(msg))
  101. }
  102. // SendSmsCode 发送短信/邮件验证码
  103. func (s *Server) SendSmsCode(c *gin.Context) {
  104. ctx := s.FromContext(c)
  105. db := s.DB()
  106. type Req struct {
  107. Type string `json:"type" binding:"required"` // phone/email
  108. Account string `json:"account" binding:"required"` // 手机号或邮箱
  109. AreaCode string `json:"areaCode"` // 国际区号 如 84, 86
  110. Scene string `json:"scene" binding:"required"` // register/login/reset/bind
  111. }
  112. var req Req
  113. if err := c.ShouldBindJSON(&req); err != nil {
  114. ctx.Fail("invalid_params")
  115. return
  116. }
  117. // 验证类型
  118. if req.Type != "phone" && req.Type != "email" {
  119. ctx.Fail("unsupported_type")
  120. return
  121. }
  122. var accountKey string // 用于缓存的key
  123. if req.Type == "phone" {
  124. // 验证手机号格式
  125. phoneRegex := regexp.MustCompile(`^\d{9,15}$`)
  126. if !phoneRegex.MatchString(req.Account) {
  127. ctx.Fail("invalid_phone")
  128. return
  129. }
  130. accountKey = req.Account
  131. if req.AreaCode != "" {
  132. accountKey = req.AreaCode + req.Account
  133. }
  134. } else {
  135. // 验证邮箱格式
  136. emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
  137. if !emailRegex.MatchString(req.Account) {
  138. ctx.Fail("invalid_email")
  139. return
  140. }
  141. accountKey = req.Account
  142. }
  143. // 防止重复发送
  144. cacheKey := fmt.Sprintf("code:%s:%s", req.Scene, accountKey)
  145. if !ctx.RepeatFilter(cacheKey, 60*time.Second) {
  146. ctx.Fail("code_send_too_fast")
  147. return
  148. }
  149. // 生成验证码
  150. n, _ := rand.Int(rand.Reader, big.NewInt(900000))
  151. code := fmt.Sprintf("%06d", n.Int64()+100000)
  152. if req.Type == "phone" {
  153. // 发送短信验证码
  154. var smsUser, smsPass, smsSign string
  155. var config entity.DtConfig
  156. if err := db.Where("`key` = ?", entity.ConfigKeySmsUser).First(&config).Error; err == nil {
  157. smsUser = config.Value
  158. }
  159. if err := db.Where("`key` = ?", entity.ConfigKeySmsPass).First(&config).Error; err == nil {
  160. smsPass = config.Value
  161. }
  162. if err := db.Where("`key` = ?", entity.ConfigKeySmsSign).First(&config).Error; err == nil {
  163. smsSign = config.Value
  164. }
  165. if smsUser != "" && smsPass != "" {
  166. if err := sendSmsBao(smsUser, smsPass, smsSign, accountKey, code); err != nil {
  167. ctx.Fail("sms_send_failed")
  168. return
  169. }
  170. } else {
  171. fmt.Printf("[DEV] SMS Code for %s: %s\n", accountKey, code)
  172. }
  173. } else {
  174. // 发送邮件验证码
  175. var smtpHost, smtpPortStr, smtpUser, smtpPass, smtpFrom string
  176. // 批量查询所有smtp配置,避免复用同一struct导致GORM主键污染
  177. var smtpConfigs []entity.DtConfig
  178. db.Where("`group` = ?", "smtp").Find(&smtpConfigs)
  179. for _, cfg := range smtpConfigs {
  180. switch cfg.Key {
  181. case entity.ConfigKeySmtpHost:
  182. smtpHost = cfg.Value
  183. case entity.ConfigKeySmtpPort:
  184. smtpPortStr = cfg.Value
  185. case entity.ConfigKeySmtpUser:
  186. smtpUser = cfg.Value
  187. case entity.ConfigKeySmtpPass:
  188. smtpPass = cfg.Value
  189. case entity.ConfigKeySmtpFrom:
  190. smtpFrom = cfg.Value
  191. }
  192. }
  193. if smtpFrom == "" {
  194. smtpFrom = "Vitiens"
  195. }
  196. if smtpHost != "" && smtpUser != "" && smtpPass != "" {
  197. smtpPort, _ := strconv.Atoi(smtpPortStr)
  198. if smtpPort == 0 {
  199. smtpPort = 587
  200. }
  201. if err := sendEmail(smtpHost, smtpPort, smtpUser, smtpPass, smtpFrom, req.Account, code); err != nil {
  202. fmt.Printf("[ERROR] Send email failed: %v\n", err)
  203. ctx.Fail("email_send_failed")
  204. return
  205. }
  206. } else {
  207. fmt.Printf("[DEV] Email Code for %s: %s\n", req.Account, code)
  208. }
  209. }
  210. // 存储验证码到Redis
  211. codeKey2 := fmt.Sprintf("verifycode:%s:%s", req.Scene, accountKey)
  212. redisclient.DefaultClient().Set(c, codeKey2, code, 5*time.Minute)
  213. ctx.OK(gin.H{
  214. "message": "code_sent",
  215. })
  216. }
  217. // Register 用户注册
  218. func (s *Server) Register(c *gin.Context) {
  219. ctx := s.FromContext(c)
  220. db := s.DB()
  221. type Req struct {
  222. Type string `json:"type" binding:"required"` // phone/email
  223. Account string `json:"account" binding:"required"` // 手机号或邮箱
  224. Code string `json:"code" binding:"required"`
  225. Password string `json:"password" binding:"required,min=6"`
  226. InviteCode string `json:"inviteCode"`
  227. AreaCode string `json:"areaCode"` // 国际区号
  228. }
  229. var req Req
  230. if err := c.ShouldBindJSON(&req); err != nil {
  231. ctx.Fail("invalid_params")
  232. return
  233. }
  234. // 验证类型
  235. if req.Type != "phone" && req.Type != "email" {
  236. ctx.Fail("unsupported_type")
  237. return
  238. }
  239. // 账号Key
  240. accountKey := req.Account
  241. if req.Type == "phone" && req.AreaCode != "" {
  242. accountKey = req.AreaCode + req.Account
  243. }
  244. // 检查验证码尝试次数(防暴力破解,5次锁定15分钟)
  245. failKey := fmt.Sprintf("code_fail:register:%s", accountKey)
  246. failCount, _ := redisclient.DefaultClient().Get(c, failKey).Int()
  247. if failCount >= 5 {
  248. ctx.Fail("too_many_attempts")
  249. return
  250. }
  251. // 验证验证码
  252. codeKey := fmt.Sprintf("verifycode:register:%s", accountKey)
  253. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  254. if err != nil || storedCode != req.Code {
  255. redisclient.DefaultClient().Incr(c, failKey)
  256. redisclient.DefaultClient().Expire(c, failKey, 15*time.Minute)
  257. ctx.Fail("invalid_code")
  258. return
  259. }
  260. redisclient.DefaultClient().Del(c, failKey)
  261. // 检查是否已注册
  262. var existUser entity.DtUser
  263. if req.Type == "phone" {
  264. if err := db.Where("phone = ?", accountKey).First(&existUser).Error; err == nil {
  265. ctx.Fail("phone_registered")
  266. return
  267. }
  268. } else {
  269. if err := db.Where("email = ?", accountKey).First(&existUser).Error; err == nil {
  270. ctx.Fail("email_registered")
  271. return
  272. }
  273. }
  274. // 查找邀请人
  275. var parentId int64 = 0
  276. if req.InviteCode != "" {
  277. var inviter entity.DtUser
  278. if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil {
  279. parentId = inviter.Id
  280. }
  281. }
  282. // 加密密码
  283. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
  284. if err != nil {
  285. ctx.Fail("register_failed")
  286. return
  287. }
  288. // 获取默认等级
  289. var defaultLevel entity.DtUserLevel
  290. db.Where("is_default = ?", 1).First(&defaultLevel)
  291. // 昵称
  292. userPrefix := ctx.I18n("user_prefix")
  293. if userPrefix == "" || userPrefix == "user_prefix" {
  294. userPrefix = "User"
  295. }
  296. // 邮箱取@前部分,手机号取后4位
  297. accountSuffix := req.Account
  298. if req.Type == "email" {
  299. if atIdx := strings.Index(req.Account, "@"); atIdx > 0 {
  300. accountSuffix = req.Account[:atIdx]
  301. }
  302. } else if len(req.Account) >= 4 {
  303. accountSuffix = req.Account[len(req.Account)-4:]
  304. }
  305. nickname := userPrefix + accountSuffix
  306. // 创建用户
  307. user := &entity.DtUser{
  308. Uid: generateUid(),
  309. Password: string(hashedPassword),
  310. Nickname: nickname,
  311. ParentId: parentId,
  312. LevelId: defaultLevel.Id,
  313. InviteCode: generateInviteCode(),
  314. Status: 1,
  315. }
  316. if req.Type == "phone" {
  317. user.Phone = accountKey
  318. } else {
  319. user.Email = accountKey
  320. // 邮箱注册时生成唯一占位手机号,避免uk_phone唯一索引冲突
  321. h := md5.New()
  322. h.Write([]byte(req.Account))
  323. emailHash := hex.EncodeToString(h.Sum(nil))[:16]
  324. user.Phone = fmt.Sprintf("email_%s", emailHash)
  325. }
  326. tx := db.Begin()
  327. if err := tx.Create(user).Error; err != nil {
  328. tx.Rollback()
  329. ctx.Fail("register_failed")
  330. return
  331. }
  332. // 更新邀请人的直推人数和团队人数
  333. if parentId > 0 {
  334. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  335. Updates(map[string]interface{}{
  336. "direct_invite_count": tx.Raw("direct_invite_count + 1"),
  337. "team_count": tx.Raw("team_count + 1"),
  338. })
  339. // 更新上级的团队人数(多级)
  340. var parent entity.DtUser
  341. if err := tx.Where("id = ?", parentId).First(&parent).Error; err == nil && parent.ParentId > 0 {
  342. tx.Model(&entity.DtUser{}).Where("id = ?", parent.ParentId).
  343. Update("team_count", tx.Raw("team_count + 1"))
  344. }
  345. }
  346. tx.Commit()
  347. // 删除验证码
  348. redisclient.DefaultClient().Del(c, codeKey)
  349. // 生成Token
  350. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  351. if err != nil {
  352. ctx.Fail("register_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. "email": user.Email,
  364. },
  365. })
  366. }
  367. // LoginByPassword 密码登录
  368. func (s *Server) LoginByPassword(c *gin.Context) {
  369. ctx := s.FromContext(c)
  370. db := s.DB()
  371. type Req struct {
  372. Type string `json:"type" binding:"required"` // phone/email
  373. Account string `json:"account" binding:"required"` // 手机号或邮箱
  374. Password string `json:"password" binding:"required"`
  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. // 检查登录尝试次数(防暴力破解,5次锁定15分钟)
  383. loginFailKey := fmt.Sprintf("login_fail:%s", req.Account)
  384. loginFailCount, _ := redisclient.DefaultClient().Get(c, loginFailKey).Int()
  385. if loginFailCount >= 5 {
  386. ctx.Fail("too_many_attempts")
  387. return
  388. }
  389. // 查找用户
  390. var user entity.DtUser
  391. if req.Type == "email" {
  392. if err := db.Where("email = ?", req.Account).First(&user).Error; err != nil {
  393. ctx.Fail("user_not_found")
  394. return
  395. }
  396. } else {
  397. fullPhone := req.Account
  398. if req.AreaCode != "" {
  399. fullPhone = req.AreaCode + req.Account
  400. }
  401. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  402. ctx.Fail("user_not_found")
  403. return
  404. }
  405. }
  406. // 检查用户状态
  407. if user.Status != 1 {
  408. ctx.Fail("user_disabled")
  409. return
  410. }
  411. // 验证密码
  412. if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
  413. redisclient.DefaultClient().Incr(c, loginFailKey)
  414. redisclient.DefaultClient().Expire(c, loginFailKey, 15*time.Minute)
  415. ctx.Fail("invalid_password")
  416. return
  417. }
  418. redisclient.DefaultClient().Del(c, loginFailKey)
  419. // 更新登录时间
  420. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  421. Update("last_login_time", time.Now().Unix())
  422. // 生成Token
  423. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  424. if err != nil {
  425. ctx.Fail("login_failed")
  426. return
  427. }
  428. ctx.OK(gin.H{
  429. "token": token,
  430. "user": gin.H{
  431. "id": user.Id,
  432. "uid": user.Uid,
  433. "nickname": user.Nickname,
  434. "avatar": user.Avatar,
  435. "phone": user.Phone,
  436. "email": user.Email,
  437. },
  438. })
  439. }
  440. // LoginBySms 短信验证码登录
  441. func (s *Server) LoginBySms(c *gin.Context) {
  442. ctx := s.FromContext(c)
  443. db := s.DB()
  444. type Req struct {
  445. Phone string `json:"phone" binding:"required"`
  446. Code string `json:"code" binding:"required"`
  447. AreaCode string `json:"areaCode"` // 国际区号
  448. }
  449. var req Req
  450. if err := c.ShouldBindJSON(&req); err != nil {
  451. ctx.Fail("invalid_params")
  452. return
  453. }
  454. // 完整手机号(带区号)
  455. fullPhone := req.Phone
  456. if req.AreaCode != "" {
  457. fullPhone = req.AreaCode + req.Phone
  458. }
  459. // 检查验证码尝试次数(防暴力破解,5次锁定15分钟)
  460. failKey := fmt.Sprintf("code_fail:login:%s", fullPhone)
  461. failCount, _ := redisclient.DefaultClient().Get(c, failKey).Int()
  462. if failCount >= 5 {
  463. ctx.Fail("too_many_attempts")
  464. return
  465. }
  466. // 验证短信验证码
  467. codeKey := fmt.Sprintf("verifycode:login:%s", fullPhone)
  468. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  469. if err != nil || storedCode != req.Code {
  470. redisclient.DefaultClient().Incr(c, failKey)
  471. redisclient.DefaultClient().Expire(c, failKey, 15*time.Minute)
  472. ctx.Fail("invalid_code")
  473. return
  474. }
  475. redisclient.DefaultClient().Del(c, failKey)
  476. // 查找用户
  477. var user entity.DtUser
  478. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  479. ctx.Fail("user_not_found")
  480. return
  481. }
  482. // 检查用户状态
  483. if user.Status != 1 {
  484. ctx.Fail("user_disabled")
  485. return
  486. }
  487. // 更新登录时间
  488. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  489. Update("last_login_time", time.Now().Unix())
  490. // 删除验证码
  491. redisclient.DefaultClient().Del(c, codeKey)
  492. // 生成Token
  493. token, err2 := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  494. if err2 != nil {
  495. ctx.Fail("login_failed")
  496. return
  497. }
  498. ctx.OK(gin.H{
  499. "token": token,
  500. "user": gin.H{
  501. "id": user.Id,
  502. "uid": user.Uid,
  503. "nickname": user.Nickname,
  504. "avatar": user.Avatar,
  505. "phone": user.Phone,
  506. "email": user.Email,
  507. },
  508. })
  509. }
  510. // ResetPassword 重置密码
  511. func (s *Server) ResetPassword(c *gin.Context) {
  512. ctx := s.FromContext(c)
  513. db := s.DB()
  514. type Req struct {
  515. Type string `json:"type" binding:"required"` // phone/email
  516. Account string `json:"account" binding:"required"` // 手机号或邮箱
  517. Code string `json:"code" binding:"required"`
  518. NewPassword string `json:"newPassword" binding:"required,min=6"`
  519. AreaCode string `json:"areaCode"` // 国际区号
  520. }
  521. var req Req
  522. if err := c.ShouldBindJSON(&req); err != nil {
  523. ctx.Fail("invalid_params")
  524. return
  525. }
  526. // 账号Key
  527. accountKey := req.Account
  528. if req.Type == "phone" && req.AreaCode != "" {
  529. accountKey = req.AreaCode + req.Account
  530. }
  531. // 检查验证码尝试次数(防暴力破解,5次锁定15分钟)
  532. failKey := fmt.Sprintf("code_fail:reset:%s", accountKey)
  533. failCount, _ := redisclient.DefaultClient().Get(c, failKey).Int()
  534. if failCount >= 5 {
  535. ctx.Fail("too_many_attempts")
  536. return
  537. }
  538. // 验证验证码
  539. codeKey := fmt.Sprintf("verifycode:reset:%s", accountKey)
  540. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  541. if err != nil || storedCode != req.Code {
  542. redisclient.DefaultClient().Incr(c, failKey)
  543. redisclient.DefaultClient().Expire(c, failKey, 15*time.Minute)
  544. ctx.Fail("invalid_code")
  545. return
  546. }
  547. redisclient.DefaultClient().Del(c, failKey)
  548. // 查找用户
  549. var user entity.DtUser
  550. if req.Type == "email" {
  551. if err := db.Where("email = ?", accountKey).First(&user).Error; err != nil {
  552. ctx.Fail("user_not_found")
  553. return
  554. }
  555. } else {
  556. if err := db.Where("phone = ?", accountKey).First(&user).Error; err != nil {
  557. ctx.Fail("user_not_found")
  558. return
  559. }
  560. }
  561. // 加密新密码
  562. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
  563. if err != nil {
  564. ctx.Fail("reset_failed")
  565. return
  566. }
  567. // 更新密码
  568. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  569. Update("password", string(hashedPassword))
  570. // 删除验证码
  571. redisclient.DefaultClient().Del(c, codeKey)
  572. ctx.OK(gin.H{
  573. "message": "password_reset_success",
  574. })
  575. }
  576. // OAuthLogin OAuth登录(Google/Zalo等)
  577. func (s *Server) OAuthLogin(c *gin.Context) {
  578. ctx := s.FromContext(c)
  579. db := s.DB()
  580. type Req struct {
  581. Provider string `json:"provider" binding:"required"` // google/zalo/telegram
  582. OpenId string `json:"openId" binding:"required"`
  583. Nickname string `json:"nickname"`
  584. Avatar string `json:"avatar"`
  585. InviteCode string `json:"inviteCode"`
  586. Extra string `json:"extra"` // Zalo: code_verifier
  587. }
  588. var req Req
  589. if err := c.ShouldBindJSON(&req); err != nil {
  590. ctx.Fail("invalid_params")
  591. return
  592. }
  593. // TikTok 特殊处理:需要用 code 换取 access_token 和用户信息
  594. if req.Provider == "tiktok" && req.Extra != "" {
  595. tiktokUser, err := s.getTiktokUserInfo(req.OpenId, req.Extra)
  596. if err != nil {
  597. fmt.Printf("TikTok get user info error: %v\n", err)
  598. ctx.Fail("tiktok_auth_failed")
  599. return
  600. }
  601. req.OpenId = tiktokUser.ID
  602. req.Nickname = tiktokUser.Name
  603. req.Avatar = tiktokUser.Picture
  604. }
  605. // Zalo 特殊处理:需要用 code 换取 access_token
  606. if req.Provider == "zalo" && req.Extra != "" {
  607. zaloUser, err := s.getZaloUserInfo(req.OpenId, req.Extra)
  608. if err != nil {
  609. fmt.Printf("Zalo get user info error: %v\n", err)
  610. ctx.Fail("zalo_auth_failed")
  611. return
  612. }
  613. req.OpenId = zaloUser.ID
  614. req.Nickname = zaloUser.Name
  615. req.Avatar = zaloUser.Picture
  616. }
  617. // 查找是否已绑定
  618. var social entity.DtUserSocial
  619. if err := db.Where("platform = ? AND account = ?", req.Provider, req.OpenId).First(&social).Error; err == nil {
  620. // 已绑定,直接登录
  621. var user entity.DtUser
  622. if err := db.Where("id = ?", social.UserId).First(&user).Error; err != nil {
  623. ctx.Fail("user_not_found")
  624. return
  625. }
  626. if user.Status != 1 {
  627. ctx.Fail("user_disabled")
  628. return
  629. }
  630. // 更新登录时间
  631. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  632. Update("last_login_time", time.Now().Unix())
  633. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  634. if err != nil {
  635. ctx.Fail("login_failed")
  636. return
  637. }
  638. ctx.OK(gin.H{
  639. "token": token,
  640. "user": gin.H{
  641. "id": user.Id,
  642. "uid": user.Uid,
  643. "nickname": user.Nickname,
  644. "avatar": user.Avatar,
  645. "phone": user.Phone,
  646. },
  647. })
  648. return
  649. }
  650. // 未绑定,创建新用户
  651. var parentId int64 = 0
  652. if req.InviteCode != "" {
  653. var inviter entity.DtUser
  654. if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil {
  655. parentId = inviter.Id
  656. }
  657. }
  658. // 获取默认等级
  659. var defaultLevel entity.DtUserLevel
  660. db.Where("is_default = ?", 1).First(&defaultLevel)
  661. nickname := req.Nickname
  662. if nickname == "" {
  663. oauthPrefix := ctx.I18n("user_prefix")
  664. if oauthPrefix == "" || oauthPrefix == "user_prefix" {
  665. oauthPrefix = "User"
  666. }
  667. nickname = req.Provider + oauthPrefix
  668. }
  669. // 为OAuth用户生成唯一占位手机号(用MD5避免截断碰撞)
  670. h := md5.New()
  671. h.Write([]byte(req.OpenId))
  672. openIdHash := hex.EncodeToString(h.Sum(nil))[:16]
  673. oauthPhone := fmt.Sprintf("oauth_%s_%s", req.Provider, openIdHash)
  674. user := &entity.DtUser{
  675. Uid: generateUid(),
  676. Phone: oauthPhone,
  677. Nickname: nickname,
  678. Avatar: req.Avatar,
  679. ParentId: parentId,
  680. LevelId: defaultLevel.Id,
  681. InviteCode: generateInviteCode(),
  682. Status: 1,
  683. }
  684. tx := db.Begin()
  685. if err := tx.Create(user).Error; err != nil {
  686. tx.Rollback()
  687. fmt.Printf("OAuth create user error: %v\n", err)
  688. // 如果是 phone 重复,说明用户已存在但缺少社交绑定,自动补绑定并登录
  689. var existingUser entity.DtUser
  690. if dbErr := db.Where("phone = ?", oauthPhone).First(&existingUser).Error; dbErr == nil {
  691. // 补创建社交绑定
  692. newSocial := entity.DtUserSocial{
  693. UserId: existingUser.Id,
  694. Platform: req.Provider,
  695. Account: req.OpenId,
  696. Nickname: req.Nickname,
  697. Avatar: req.Avatar,
  698. Extra: "{}",
  699. }
  700. db.Create(&newSocial)
  701. // 更新登录时间
  702. db.Model(&entity.DtUser{}).Where("id = ?", existingUser.Id).
  703. Update("last_login_time", time.Now().Unix())
  704. existToken, tokenErr := middleware.GenerateJWT(middleware.Member{ID: existingUser.Id, Uid: existingUser.Uid})
  705. if tokenErr != nil {
  706. ctx.Fail("login_failed")
  707. return
  708. }
  709. ctx.OK(gin.H{
  710. "token": existToken,
  711. "user": gin.H{
  712. "id": existingUser.Id,
  713. "uid": existingUser.Uid,
  714. "nickname": existingUser.Nickname,
  715. "avatar": existingUser.Avatar,
  716. "phone": existingUser.Phone,
  717. "email": existingUser.Email,
  718. },
  719. })
  720. return
  721. }
  722. ctx.Fail("login_failed")
  723. return
  724. }
  725. // 创建社交账号绑定
  726. social = entity.DtUserSocial{
  727. UserId: user.Id,
  728. Platform: req.Provider,
  729. Account: req.OpenId,
  730. Nickname: req.Nickname,
  731. Avatar: req.Avatar,
  732. Extra: "{}",
  733. }
  734. if err := tx.Create(&social).Error; err != nil {
  735. tx.Rollback()
  736. fmt.Printf("OAuth create social error: %v\n", err)
  737. ctx.Fail("login_failed")
  738. return
  739. }
  740. // 更新邀请人统计
  741. if parentId > 0 {
  742. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  743. Updates(map[string]interface{}{
  744. "direct_invite_count": tx.Raw("direct_invite_count + 1"),
  745. "team_count": tx.Raw("team_count + 1"),
  746. })
  747. }
  748. tx.Commit()
  749. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  750. if err != nil {
  751. fmt.Printf("OAuth generate JWT error: %v\n", err)
  752. ctx.Fail("login_failed")
  753. return
  754. }
  755. ctx.OK(gin.H{
  756. "token": token,
  757. "user": gin.H{
  758. "id": user.Id,
  759. "uid": user.Uid,
  760. "nickname": user.Nickname,
  761. "avatar": user.Avatar,
  762. "phone": user.Phone,
  763. "email": user.Email,
  764. },
  765. "isNew": true,
  766. })
  767. }
  768. // ZaloUser Zalo用户信息
  769. type ZaloUser struct {
  770. ID string `json:"id"`
  771. Name string `json:"name"`
  772. Picture string `json:"picture"`
  773. }
  774. // getZaloUserInfo 使用code获取Zalo用户信息
  775. func (s *Server) getZaloUserInfo(code, codeVerifier string) (*ZaloUser, error) {
  776. db := s.DB()
  777. // 获取 Zalo App ID 和 Secret
  778. var appIdConfig, secretConfig entity.DtConfig
  779. if err := db.Where("`key` = ?", entity.ConfigKeyZaloAppId).First(&appIdConfig).Error; err != nil {
  780. return nil, fmt.Errorf("zalo app_id not configured")
  781. }
  782. if err := db.Where("`key` = ?", entity.ConfigKeyZaloSecret).First(&secretConfig).Error; err != nil {
  783. return nil, fmt.Errorf("zalo secret not configured")
  784. }
  785. // 1. 用 code 换取 access_token
  786. tokenUrl := "https://oauth.zaloapp.com/v4/access_token"
  787. data := url.Values{}
  788. data.Set("app_id", appIdConfig.Value)
  789. data.Set("code", code)
  790. data.Set("code_verifier", codeVerifier)
  791. data.Set("grant_type", "authorization_code")
  792. req, _ := http.NewRequest("POST", tokenUrl, strings.NewReader(data.Encode()))
  793. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  794. req.Header.Set("secret_key", secretConfig.Value)
  795. client := &http.Client{Timeout: 10 * time.Second}
  796. resp, err := client.Do(req)
  797. if err != nil {
  798. return nil, fmt.Errorf("request token failed: %v", err)
  799. }
  800. defer resp.Body.Close()
  801. body, _ := io.ReadAll(resp.Body)
  802. fmt.Printf("Zalo token response: %s\n", string(body))
  803. var tokenResp struct {
  804. AccessToken string `json:"access_token"`
  805. ExpiresIn string `json:"expires_in"` // Zalo 返回的是字符串类型
  806. Error int `json:"error"`
  807. Message string `json:"message"`
  808. }
  809. if err := json.Unmarshal(body, &tokenResp); err != nil {
  810. return nil, fmt.Errorf("parse token response failed: %v", err)
  811. }
  812. if tokenResp.Error != 0 || tokenResp.AccessToken == "" {
  813. return nil, fmt.Errorf("get token failed: %s", tokenResp.Message)
  814. }
  815. // 2. 用 access_token 获取用户信息
  816. userUrl := "https://graph.zalo.me/v2.0/me?fields=id,name,picture"
  817. userReq, _ := http.NewRequest("GET", userUrl, nil)
  818. userReq.Header.Set("access_token", tokenResp.AccessToken)
  819. userResp, err := client.Do(userReq)
  820. if err != nil {
  821. return nil, fmt.Errorf("request user info failed: %v", err)
  822. }
  823. defer userResp.Body.Close()
  824. userBody, _ := io.ReadAll(userResp.Body)
  825. fmt.Printf("Zalo user response: %s\n", string(userBody))
  826. // Zalo API 错误响应格式: {"error": -501, "message": "..."}
  827. // Zalo API 成功响应格式: {"id": "...", "name": "...", "picture": {...}}
  828. var errorResp struct {
  829. Error int `json:"error"`
  830. Message string `json:"message"`
  831. }
  832. if err := json.Unmarshal(userBody, &errorResp); err == nil && errorResp.Error != 0 {
  833. return nil, fmt.Errorf("get user info failed: [%d] %s", errorResp.Error, errorResp.Message)
  834. }
  835. var userInfo struct {
  836. ID string `json:"id"`
  837. Name string `json:"name"`
  838. Picture struct {
  839. Data struct {
  840. URL string `json:"url"`
  841. } `json:"data"`
  842. } `json:"picture"`
  843. }
  844. if err := json.Unmarshal(userBody, &userInfo); err != nil {
  845. return nil, fmt.Errorf("parse user info failed: %v", err)
  846. }
  847. if userInfo.ID == "" {
  848. return nil, fmt.Errorf("get user info failed: empty user id")
  849. }
  850. return &ZaloUser{
  851. ID: userInfo.ID,
  852. Name: userInfo.Name,
  853. Picture: userInfo.Picture.Data.URL,
  854. }, nil
  855. }
  856. // TiktokUser TikTok用户信息
  857. type TiktokUser struct {
  858. ID string `json:"id"`
  859. Name string `json:"name"`
  860. Picture string `json:"picture"`
  861. }
  862. // getTiktokUserInfo 使用code获取TikTok用户信息
  863. func (s *Server) getTiktokUserInfo(code, redirectUri string) (*TiktokUser, error) {
  864. db := s.DB()
  865. // 获取 TikTok Client Key 和 Secret
  866. var clientKeyConfig, clientSecretConfig entity.DtConfig
  867. if err := db.Where("`key` = ?", entity.ConfigKeyTiktokClientKey).First(&clientKeyConfig).Error; err != nil {
  868. return nil, fmt.Errorf("tiktok client_key not configured")
  869. }
  870. if err := db.Where("`key` = ?", entity.ConfigKeyTiktokClientSecret).First(&clientSecretConfig).Error; err != nil {
  871. return nil, fmt.Errorf("tiktok client_secret not configured")
  872. }
  873. // 1. 用 code 换取 access_token
  874. tokenUrl := "https://open.tiktokapis.com/v2/oauth/token/"
  875. data := url.Values{}
  876. data.Set("client_key", clientKeyConfig.Value)
  877. data.Set("client_secret", clientSecretConfig.Value)
  878. data.Set("code", code)
  879. data.Set("grant_type", "authorization_code")
  880. data.Set("redirect_uri", redirectUri)
  881. req, _ := http.NewRequest("POST", tokenUrl, strings.NewReader(data.Encode()))
  882. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  883. client := &http.Client{Timeout: 10 * time.Second}
  884. resp, err := client.Do(req)
  885. if err != nil {
  886. return nil, fmt.Errorf("request token failed: %v", err)
  887. }
  888. defer resp.Body.Close()
  889. body, _ := io.ReadAll(resp.Body)
  890. fmt.Printf("TikTok token response: %s\n", string(body))
  891. var tokenResp struct {
  892. AccessToken string `json:"access_token"`
  893. OpenId string `json:"open_id"`
  894. ExpiresIn int `json:"expires_in"`
  895. TokenType string `json:"token_type"`
  896. Error string `json:"error"`
  897. ErrorDesc string `json:"error_description"`
  898. }
  899. if err := json.Unmarshal(body, &tokenResp); err != nil {
  900. return nil, fmt.Errorf("parse token response failed: %v", err)
  901. }
  902. if tokenResp.Error != "" || tokenResp.AccessToken == "" {
  903. return nil, fmt.Errorf("get token failed: %s - %s", tokenResp.Error, tokenResp.ErrorDesc)
  904. }
  905. // 2. 用 access_token 获取用户信息
  906. userUrl := "https://open.tiktokapis.com/v2/user/info/?fields=open_id,display_name,avatar_url"
  907. userReq, _ := http.NewRequest("GET", userUrl, nil)
  908. userReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken)
  909. userResp, err := client.Do(userReq)
  910. if err != nil {
  911. return nil, fmt.Errorf("request user info failed: %v", err)
  912. }
  913. defer userResp.Body.Close()
  914. userBody, _ := io.ReadAll(userResp.Body)
  915. fmt.Printf("TikTok user response: %s\n", string(userBody))
  916. var userInfoResp struct {
  917. Data struct {
  918. User struct {
  919. OpenId string `json:"open_id"`
  920. DisplayName string `json:"display_name"`
  921. AvatarUrl string `json:"avatar_url"`
  922. } `json:"user"`
  923. } `json:"data"`
  924. Error struct {
  925. Code string `json:"code"`
  926. Message string `json:"message"`
  927. } `json:"error"`
  928. }
  929. if err := json.Unmarshal(userBody, &userInfoResp); err != nil {
  930. return nil, fmt.Errorf("parse user info failed: %v", err)
  931. }
  932. if userInfoResp.Error.Code != "" && userInfoResp.Error.Code != "ok" {
  933. return nil, fmt.Errorf("get user info failed: [%s] %s", userInfoResp.Error.Code, userInfoResp.Error.Message)
  934. }
  935. openId := userInfoResp.Data.User.OpenId
  936. if openId == "" {
  937. openId = tokenResp.OpenId
  938. }
  939. if openId == "" {
  940. return nil, fmt.Errorf("get user info failed: empty open_id")
  941. }
  942. return &TiktokUser{
  943. ID: openId,
  944. Name: userInfoResp.Data.User.DisplayName,
  945. Picture: userInfoResp.Data.User.AvatarUrl,
  946. }, nil
  947. }