auth.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  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. nickname := userPrefix
  297. if len(req.Account) >= 4 {
  298. nickname += req.Account[len(req.Account)-4:]
  299. } else {
  300. nickname += req.Account
  301. }
  302. // 创建用户
  303. user := &entity.DtUser{
  304. Uid: generateUid(),
  305. Password: string(hashedPassword),
  306. Nickname: nickname,
  307. ParentId: parentId,
  308. LevelId: defaultLevel.Id,
  309. InviteCode: generateInviteCode(),
  310. Status: 1,
  311. }
  312. if req.Type == "phone" {
  313. user.Phone = accountKey
  314. } else {
  315. user.Email = accountKey
  316. // 邮箱注册时生成唯一占位手机号,避免uk_phone唯一索引冲突
  317. h := md5.New()
  318. h.Write([]byte(req.Account))
  319. emailHash := hex.EncodeToString(h.Sum(nil))[:16]
  320. user.Phone = fmt.Sprintf("email_%s", emailHash)
  321. }
  322. tx := db.Begin()
  323. if err := tx.Create(user).Error; err != nil {
  324. tx.Rollback()
  325. ctx.Fail("register_failed")
  326. return
  327. }
  328. // 更新邀请人的直推人数和团队人数
  329. if parentId > 0 {
  330. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  331. Updates(map[string]interface{}{
  332. "direct_invite_count": tx.Raw("direct_invite_count + 1"),
  333. "team_count": tx.Raw("team_count + 1"),
  334. })
  335. // 更新上级的团队人数(多级)
  336. var parent entity.DtUser
  337. if err := tx.Where("id = ?", parentId).First(&parent).Error; err == nil && parent.ParentId > 0 {
  338. tx.Model(&entity.DtUser{}).Where("id = ?", parent.ParentId).
  339. Update("team_count", tx.Raw("team_count + 1"))
  340. }
  341. }
  342. tx.Commit()
  343. // 删除验证码
  344. redisclient.DefaultClient().Del(c, codeKey)
  345. // 生成Token
  346. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  347. if err != nil {
  348. ctx.Fail("register_failed")
  349. return
  350. }
  351. ctx.OK(gin.H{
  352. "token": token,
  353. "user": gin.H{
  354. "id": user.Id,
  355. "uid": user.Uid,
  356. "nickname": user.Nickname,
  357. "avatar": user.Avatar,
  358. "phone": user.Phone,
  359. "email": user.Email,
  360. },
  361. })
  362. }
  363. // LoginByPassword 密码登录
  364. func (s *Server) LoginByPassword(c *gin.Context) {
  365. ctx := s.FromContext(c)
  366. db := s.DB()
  367. type Req struct {
  368. Type string `json:"type" binding:"required"` // phone/email
  369. Account string `json:"account" binding:"required"` // 手机号或邮箱
  370. Password string `json:"password" binding:"required"`
  371. AreaCode string `json:"areaCode"` // 国际区号
  372. }
  373. var req Req
  374. if err := c.ShouldBindJSON(&req); err != nil {
  375. ctx.Fail("invalid_params")
  376. return
  377. }
  378. // 检查登录尝试次数(防暴力破解,5次锁定15分钟)
  379. loginFailKey := fmt.Sprintf("login_fail:%s", req.Account)
  380. loginFailCount, _ := redisclient.DefaultClient().Get(c, loginFailKey).Int()
  381. if loginFailCount >= 5 {
  382. ctx.Fail("too_many_attempts")
  383. return
  384. }
  385. // 查找用户
  386. var user entity.DtUser
  387. if req.Type == "email" {
  388. if err := db.Where("email = ?", req.Account).First(&user).Error; err != nil {
  389. ctx.Fail("user_not_found")
  390. return
  391. }
  392. } else {
  393. fullPhone := req.Account
  394. if req.AreaCode != "" {
  395. fullPhone = req.AreaCode + req.Account
  396. }
  397. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  398. ctx.Fail("user_not_found")
  399. return
  400. }
  401. }
  402. // 检查用户状态
  403. if user.Status != 1 {
  404. ctx.Fail("user_disabled")
  405. return
  406. }
  407. // 验证密码
  408. if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
  409. redisclient.DefaultClient().Incr(c, loginFailKey)
  410. redisclient.DefaultClient().Expire(c, loginFailKey, 15*time.Minute)
  411. ctx.Fail("invalid_password")
  412. return
  413. }
  414. redisclient.DefaultClient().Del(c, loginFailKey)
  415. // 更新登录时间
  416. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  417. Update("last_login_time", time.Now().Unix())
  418. // 生成Token
  419. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  420. if err != nil {
  421. ctx.Fail("login_failed")
  422. return
  423. }
  424. ctx.OK(gin.H{
  425. "token": token,
  426. "user": gin.H{
  427. "id": user.Id,
  428. "uid": user.Uid,
  429. "nickname": user.Nickname,
  430. "avatar": user.Avatar,
  431. "phone": user.Phone,
  432. "email": user.Email,
  433. },
  434. })
  435. }
  436. // LoginBySms 短信验证码登录
  437. func (s *Server) LoginBySms(c *gin.Context) {
  438. ctx := s.FromContext(c)
  439. db := s.DB()
  440. type Req struct {
  441. Phone string `json:"phone" binding:"required"`
  442. Code string `json:"code" binding:"required"`
  443. AreaCode string `json:"areaCode"` // 国际区号
  444. }
  445. var req Req
  446. if err := c.ShouldBindJSON(&req); err != nil {
  447. ctx.Fail("invalid_params")
  448. return
  449. }
  450. // 完整手机号(带区号)
  451. fullPhone := req.Phone
  452. if req.AreaCode != "" {
  453. fullPhone = req.AreaCode + req.Phone
  454. }
  455. // 检查验证码尝试次数(防暴力破解,5次锁定15分钟)
  456. failKey := fmt.Sprintf("code_fail:login:%s", fullPhone)
  457. failCount, _ := redisclient.DefaultClient().Get(c, failKey).Int()
  458. if failCount >= 5 {
  459. ctx.Fail("too_many_attempts")
  460. return
  461. }
  462. // 验证短信验证码
  463. codeKey := fmt.Sprintf("verifycode:login:%s", fullPhone)
  464. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  465. if err != nil || storedCode != req.Code {
  466. redisclient.DefaultClient().Incr(c, failKey)
  467. redisclient.DefaultClient().Expire(c, failKey, 15*time.Minute)
  468. ctx.Fail("invalid_code")
  469. return
  470. }
  471. redisclient.DefaultClient().Del(c, failKey)
  472. // 查找用户
  473. var user entity.DtUser
  474. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  475. ctx.Fail("user_not_found")
  476. return
  477. }
  478. // 检查用户状态
  479. if user.Status != 1 {
  480. ctx.Fail("user_disabled")
  481. return
  482. }
  483. // 更新登录时间
  484. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  485. Update("last_login_time", time.Now().Unix())
  486. // 删除验证码
  487. redisclient.DefaultClient().Del(c, codeKey)
  488. // 生成Token
  489. token, err2 := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  490. if err2 != nil {
  491. ctx.Fail("login_failed")
  492. return
  493. }
  494. ctx.OK(gin.H{
  495. "token": token,
  496. "user": gin.H{
  497. "id": user.Id,
  498. "uid": user.Uid,
  499. "nickname": user.Nickname,
  500. "avatar": user.Avatar,
  501. "phone": user.Phone,
  502. "email": user.Email,
  503. },
  504. })
  505. }
  506. // ResetPassword 重置密码
  507. func (s *Server) ResetPassword(c *gin.Context) {
  508. ctx := s.FromContext(c)
  509. db := s.DB()
  510. type Req struct {
  511. Type string `json:"type" binding:"required"` // phone/email
  512. Account string `json:"account" binding:"required"` // 手机号或邮箱
  513. Code string `json:"code" binding:"required"`
  514. NewPassword string `json:"newPassword" binding:"required,min=6"`
  515. AreaCode string `json:"areaCode"` // 国际区号
  516. }
  517. var req Req
  518. if err := c.ShouldBindJSON(&req); err != nil {
  519. ctx.Fail("invalid_params")
  520. return
  521. }
  522. // 账号Key
  523. accountKey := req.Account
  524. if req.Type == "phone" && req.AreaCode != "" {
  525. accountKey = req.AreaCode + req.Account
  526. }
  527. // 检查验证码尝试次数(防暴力破解,5次锁定15分钟)
  528. failKey := fmt.Sprintf("code_fail:reset:%s", accountKey)
  529. failCount, _ := redisclient.DefaultClient().Get(c, failKey).Int()
  530. if failCount >= 5 {
  531. ctx.Fail("too_many_attempts")
  532. return
  533. }
  534. // 验证验证码
  535. codeKey := fmt.Sprintf("verifycode:reset:%s", accountKey)
  536. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  537. if err != nil || storedCode != req.Code {
  538. redisclient.DefaultClient().Incr(c, failKey)
  539. redisclient.DefaultClient().Expire(c, failKey, 15*time.Minute)
  540. ctx.Fail("invalid_code")
  541. return
  542. }
  543. redisclient.DefaultClient().Del(c, failKey)
  544. // 查找用户
  545. var user entity.DtUser
  546. if req.Type == "email" {
  547. if err := db.Where("email = ?", accountKey).First(&user).Error; err != nil {
  548. ctx.Fail("user_not_found")
  549. return
  550. }
  551. } else {
  552. if err := db.Where("phone = ?", accountKey).First(&user).Error; err != nil {
  553. ctx.Fail("user_not_found")
  554. return
  555. }
  556. }
  557. // 加密新密码
  558. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
  559. if err != nil {
  560. ctx.Fail("reset_failed")
  561. return
  562. }
  563. // 更新密码
  564. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  565. Update("password", string(hashedPassword))
  566. // 删除验证码
  567. redisclient.DefaultClient().Del(c, codeKey)
  568. ctx.OK(gin.H{
  569. "message": "password_reset_success",
  570. })
  571. }
  572. // OAuthLogin OAuth登录(Google/Zalo等)
  573. func (s *Server) OAuthLogin(c *gin.Context) {
  574. ctx := s.FromContext(c)
  575. db := s.DB()
  576. type Req struct {
  577. Provider string `json:"provider" binding:"required"` // google/zalo/telegram
  578. OpenId string `json:"openId" binding:"required"`
  579. Nickname string `json:"nickname"`
  580. Avatar string `json:"avatar"`
  581. InviteCode string `json:"inviteCode"`
  582. Extra string `json:"extra"` // Zalo: code_verifier
  583. }
  584. var req Req
  585. if err := c.ShouldBindJSON(&req); err != nil {
  586. ctx.Fail("invalid_params")
  587. return
  588. }
  589. // Zalo 特殊处理:需要用 code 换取 access_token
  590. if req.Provider == "zalo" && req.Extra != "" {
  591. zaloUser, err := s.getZaloUserInfo(req.OpenId, req.Extra)
  592. if err != nil {
  593. fmt.Printf("Zalo get user info error: %v\n", err)
  594. ctx.Fail("zalo_auth_failed")
  595. return
  596. }
  597. req.OpenId = zaloUser.ID
  598. req.Nickname = zaloUser.Name
  599. req.Avatar = zaloUser.Picture
  600. }
  601. // 查找是否已绑定
  602. var social entity.DtUserSocial
  603. if err := db.Where("platform = ? AND account = ?", req.Provider, req.OpenId).First(&social).Error; err == nil {
  604. // 已绑定,直接登录
  605. var user entity.DtUser
  606. if err := db.Where("id = ?", social.UserId).First(&user).Error; err != nil {
  607. ctx.Fail("user_not_found")
  608. return
  609. }
  610. if user.Status != 1 {
  611. ctx.Fail("user_disabled")
  612. return
  613. }
  614. // 更新登录时间
  615. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  616. Update("last_login_time", time.Now().Unix())
  617. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  618. if err != nil {
  619. ctx.Fail("login_failed")
  620. return
  621. }
  622. ctx.OK(gin.H{
  623. "token": token,
  624. "user": gin.H{
  625. "id": user.Id,
  626. "uid": user.Uid,
  627. "nickname": user.Nickname,
  628. "avatar": user.Avatar,
  629. "phone": user.Phone,
  630. },
  631. })
  632. return
  633. }
  634. // 未绑定,创建新用户
  635. var parentId int64 = 0
  636. if req.InviteCode != "" {
  637. var inviter entity.DtUser
  638. if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil {
  639. parentId = inviter.Id
  640. }
  641. }
  642. // 获取默认等级
  643. var defaultLevel entity.DtUserLevel
  644. db.Where("is_default = ?", 1).First(&defaultLevel)
  645. nickname := req.Nickname
  646. if nickname == "" {
  647. oauthPrefix := ctx.I18n("user_prefix")
  648. if oauthPrefix == "" || oauthPrefix == "user_prefix" {
  649. oauthPrefix = "User"
  650. }
  651. nickname = req.Provider + oauthPrefix
  652. }
  653. // 为OAuth用户生成唯一占位手机号(用MD5避免截断碰撞)
  654. h := md5.New()
  655. h.Write([]byte(req.OpenId))
  656. openIdHash := hex.EncodeToString(h.Sum(nil))[:16]
  657. oauthPhone := fmt.Sprintf("oauth_%s_%s", req.Provider, openIdHash)
  658. user := &entity.DtUser{
  659. Uid: generateUid(),
  660. Phone: oauthPhone,
  661. Nickname: nickname,
  662. Avatar: req.Avatar,
  663. ParentId: parentId,
  664. LevelId: defaultLevel.Id,
  665. InviteCode: generateInviteCode(),
  666. Status: 1,
  667. }
  668. tx := db.Begin()
  669. if err := tx.Create(user).Error; err != nil {
  670. tx.Rollback()
  671. fmt.Printf("OAuth create user error: %v\n", err)
  672. ctx.Fail("login_failed")
  673. return
  674. }
  675. // 创建社交账号绑定
  676. social = entity.DtUserSocial{
  677. UserId: user.Id,
  678. Platform: req.Provider,
  679. Account: req.OpenId,
  680. Nickname: req.Nickname,
  681. Avatar: req.Avatar,
  682. Extra: "{}",
  683. }
  684. if err := tx.Create(&social).Error; err != nil {
  685. tx.Rollback()
  686. fmt.Printf("OAuth create social error: %v\n", err)
  687. ctx.Fail("login_failed")
  688. return
  689. }
  690. // 更新邀请人统计
  691. if parentId > 0 {
  692. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  693. Updates(map[string]interface{}{
  694. "direct_invite_count": tx.Raw("direct_invite_count + 1"),
  695. "team_count": tx.Raw("team_count + 1"),
  696. })
  697. }
  698. tx.Commit()
  699. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  700. if err != nil {
  701. fmt.Printf("OAuth generate JWT error: %v\n", err)
  702. ctx.Fail("login_failed")
  703. return
  704. }
  705. ctx.OK(gin.H{
  706. "token": token,
  707. "user": gin.H{
  708. "id": user.Id,
  709. "uid": user.Uid,
  710. "nickname": user.Nickname,
  711. "avatar": user.Avatar,
  712. "phone": user.Phone,
  713. "email": user.Email,
  714. },
  715. "isNew": true,
  716. })
  717. }
  718. // ZaloUser Zalo用户信息
  719. type ZaloUser struct {
  720. ID string `json:"id"`
  721. Name string `json:"name"`
  722. Picture string `json:"picture"`
  723. }
  724. // getZaloUserInfo 使用code获取Zalo用户信息
  725. func (s *Server) getZaloUserInfo(code, codeVerifier string) (*ZaloUser, error) {
  726. db := s.DB()
  727. // 获取 Zalo App ID 和 Secret
  728. var appIdConfig, secretConfig entity.DtConfig
  729. if err := db.Where("`key` = ?", entity.ConfigKeyZaloAppId).First(&appIdConfig).Error; err != nil {
  730. return nil, fmt.Errorf("zalo app_id not configured")
  731. }
  732. if err := db.Where("`key` = ?", entity.ConfigKeyZaloSecret).First(&secretConfig).Error; err != nil {
  733. return nil, fmt.Errorf("zalo secret not configured")
  734. }
  735. // 1. 用 code 换取 access_token
  736. tokenUrl := "https://oauth.zaloapp.com/v4/access_token"
  737. data := url.Values{}
  738. data.Set("app_id", appIdConfig.Value)
  739. data.Set("code", code)
  740. data.Set("code_verifier", codeVerifier)
  741. data.Set("grant_type", "authorization_code")
  742. req, _ := http.NewRequest("POST", tokenUrl, strings.NewReader(data.Encode()))
  743. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  744. req.Header.Set("secret_key", secretConfig.Value)
  745. client := &http.Client{Timeout: 10 * time.Second}
  746. resp, err := client.Do(req)
  747. if err != nil {
  748. return nil, fmt.Errorf("request token failed: %v", err)
  749. }
  750. defer resp.Body.Close()
  751. body, _ := io.ReadAll(resp.Body)
  752. fmt.Printf("Zalo token response: %s\n", string(body))
  753. var tokenResp struct {
  754. AccessToken string `json:"access_token"`
  755. ExpiresIn string `json:"expires_in"` // Zalo 返回的是字符串类型
  756. Error int `json:"error"`
  757. Message string `json:"message"`
  758. }
  759. if err := json.Unmarshal(body, &tokenResp); err != nil {
  760. return nil, fmt.Errorf("parse token response failed: %v", err)
  761. }
  762. if tokenResp.Error != 0 || tokenResp.AccessToken == "" {
  763. return nil, fmt.Errorf("get token failed: %s", tokenResp.Message)
  764. }
  765. // 2. 用 access_token 获取用户信息
  766. userUrl := "https://graph.zalo.me/v2.0/me?fields=id,name,picture"
  767. userReq, _ := http.NewRequest("GET", userUrl, nil)
  768. userReq.Header.Set("access_token", tokenResp.AccessToken)
  769. userResp, err := client.Do(userReq)
  770. if err != nil {
  771. return nil, fmt.Errorf("request user info failed: %v", err)
  772. }
  773. defer userResp.Body.Close()
  774. userBody, _ := io.ReadAll(userResp.Body)
  775. fmt.Printf("Zalo user response: %s\n", string(userBody))
  776. // Zalo API 错误响应格式: {"error": -501, "message": "..."}
  777. // Zalo API 成功响应格式: {"id": "...", "name": "...", "picture": {...}}
  778. var errorResp struct {
  779. Error int `json:"error"`
  780. Message string `json:"message"`
  781. }
  782. if err := json.Unmarshal(userBody, &errorResp); err == nil && errorResp.Error != 0 {
  783. return nil, fmt.Errorf("get user info failed: [%d] %s", errorResp.Error, errorResp.Message)
  784. }
  785. var userInfo struct {
  786. ID string `json:"id"`
  787. Name string `json:"name"`
  788. Picture struct {
  789. Data struct {
  790. URL string `json:"url"`
  791. } `json:"data"`
  792. } `json:"picture"`
  793. }
  794. if err := json.Unmarshal(userBody, &userInfo); err != nil {
  795. return nil, fmt.Errorf("parse user info failed: %v", err)
  796. }
  797. if userInfo.ID == "" {
  798. return nil, fmt.Errorf("get user info failed: empty user id")
  799. }
  800. return &ZaloUser{
  801. ID: userInfo.ID,
  802. Name: userInfo.Name,
  803. Picture: userInfo.Picture.Data.URL,
  804. }, nil
  805. }