auth.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  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/url"
  15. "regexp"
  16. "strings"
  17. "time"
  18. "github.com/gin-gonic/gin"
  19. "golang.org/x/crypto/bcrypt"
  20. )
  21. // generateInviteCode 生成邀请码
  22. func generateInviteCode() string {
  23. const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  24. code := make([]byte, 8)
  25. for i := range code {
  26. n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
  27. code[i] = charset[n.Int64()]
  28. }
  29. return string(code)
  30. }
  31. // generateUid 生成用户UID
  32. func generateUid() string {
  33. return fmt.Sprintf("DT%d", time.Now().UnixNano()/1000000)
  34. }
  35. // sendSmsBao 调用短信宝发送短信
  36. func sendSmsBao(user, pass, sign, phone, code string) error {
  37. // 短信宝API接口
  38. // 国内: http://api.smsbao.com/sms
  39. // 国际: http://api.smsbao.com/wsms
  40. apiUrl := "http://api.smsbao.com/wsms" // 默认使用国际接口
  41. // 国内手机号使用国内接口
  42. if strings.HasPrefix(phone, "86") || (!strings.HasPrefix(phone, "+") && len(phone) == 11) {
  43. apiUrl = "http://api.smsbao.com/sms"
  44. // 国内手机号去掉区号前缀
  45. if strings.HasPrefix(phone, "86") {
  46. phone = phone[2:]
  47. }
  48. }
  49. // MD5加密密码
  50. h := md5.New()
  51. h.Write([]byte(pass))
  52. passHash := hex.EncodeToString(h.Sum(nil))
  53. // 短信内容
  54. content := fmt.Sprintf("【%s】您的验证码是%s,在5分钟内有效。", sign, code)
  55. // 构建请求参数
  56. params := url.Values{}
  57. params.Set("u", user)
  58. params.Set("p", passHash)
  59. params.Set("m", phone)
  60. params.Set("c", content)
  61. // 发送请求
  62. resp, err := http.Get(apiUrl + "?" + params.Encode())
  63. if err != nil {
  64. return fmt.Errorf("sms request failed: %v", err)
  65. }
  66. defer resp.Body.Close()
  67. body, _ := io.ReadAll(resp.Body)
  68. result := strings.TrimSpace(string(body))
  69. // 短信宝返回码: 0=成功, 其他=失败
  70. if result != "0" {
  71. return fmt.Errorf("smsbao error: %s", result)
  72. }
  73. return nil
  74. }
  75. // SendSmsCode 发送短信验证码
  76. func (s *Server) SendSmsCode(c *gin.Context) {
  77. ctx := s.FromContext(c)
  78. db := s.DB()
  79. type Req struct {
  80. Type string `json:"type" binding:"required"` // phone/email
  81. Account string `json:"account" binding:"required"` // 手机号或邮箱
  82. AreaCode string `json:"areaCode"` // 国际区号 如 84, 86
  83. Scene string `json:"scene" binding:"required"` // register/login/reset/bind
  84. }
  85. var req Req
  86. if err := c.ShouldBindJSON(&req); err != nil {
  87. ctx.Fail("invalid_params")
  88. return
  89. }
  90. // 目前只支持手机号
  91. if req.Type != "phone" {
  92. ctx.Fail("unsupported_type")
  93. return
  94. }
  95. // 验证手机号格式
  96. phoneRegex := regexp.MustCompile(`^\d{9,15}$`)
  97. if !phoneRegex.MatchString(req.Account) {
  98. ctx.Fail("invalid_phone")
  99. return
  100. }
  101. // 完整手机号(带区号)
  102. fullPhone := req.Account
  103. if req.AreaCode != "" {
  104. fullPhone = req.AreaCode + req.Account
  105. }
  106. // 防止重复发送
  107. cacheKey := fmt.Sprintf("sms:%s:%s", req.Scene, fullPhone)
  108. if !ctx.RepeatFilter(cacheKey, 60*time.Second) {
  109. ctx.Fail("sms_send_too_fast")
  110. return
  111. }
  112. // 生成验证码
  113. n, _ := rand.Int(rand.Reader, big.NewInt(900000))
  114. code := fmt.Sprintf("%06d", n.Int64()+100000)
  115. // 获取短信宝配置
  116. var smsUser, smsPass, smsSign string
  117. var config entity.DtConfig
  118. if err := db.Where("`key` = ?", entity.ConfigKeySmsUser).First(&config).Error; err == nil {
  119. smsUser = config.Value
  120. }
  121. if err := db.Where("`key` = ?", entity.ConfigKeySmsPass).First(&config).Error; err == nil {
  122. smsPass = config.Value
  123. }
  124. if err := db.Where("`key` = ?", entity.ConfigKeySmsSign).First(&config).Error; err == nil {
  125. smsSign = config.Value
  126. }
  127. // 调用短信宝发送验证码
  128. if smsUser != "" && smsPass != "" {
  129. if err := sendSmsBao(smsUser, smsPass, smsSign, fullPhone, code); err != nil {
  130. ctx.Fail("sms_send_failed")
  131. return
  132. }
  133. } else {
  134. // 开发模式:打印验证码到日志
  135. fmt.Printf("[DEV] SMS Code for %s: %s\n", fullPhone, code)
  136. }
  137. // 存储验证码到Redis
  138. codeKey := fmt.Sprintf("smscode:%s:%s", req.Scene, fullPhone)
  139. redisclient.DefaultClient().Set(c, codeKey, code, 5*time.Minute)
  140. ctx.OK(gin.H{
  141. "message": "sms_sent",
  142. })
  143. }
  144. // Register 用户注册
  145. func (s *Server) Register(c *gin.Context) {
  146. ctx := s.FromContext(c)
  147. db := s.DB()
  148. type Req struct {
  149. Type string `json:"type" binding:"required"` // phone/email
  150. Account string `json:"account" binding:"required"` // 手机号或邮箱
  151. Code string `json:"code" binding:"required"`
  152. Password string `json:"password" binding:"required,min=6"`
  153. InviteCode string `json:"inviteCode"`
  154. AreaCode string `json:"areaCode"` // 国际区号
  155. }
  156. var req Req
  157. if err := c.ShouldBindJSON(&req); err != nil {
  158. ctx.Fail("invalid_params")
  159. return
  160. }
  161. // 目前只支持手机号注册
  162. if req.Type != "phone" {
  163. ctx.Fail("unsupported_type")
  164. return
  165. }
  166. // 完整手机号(带区号)
  167. fullPhone := req.Account
  168. if req.AreaCode != "" {
  169. fullPhone = req.AreaCode + req.Account
  170. }
  171. // 验证短信验证码
  172. codeKey := fmt.Sprintf("smscode:register:%s", fullPhone)
  173. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  174. if err != nil || storedCode != req.Code {
  175. ctx.Fail("invalid_code")
  176. return
  177. }
  178. // 检查手机号是否已注册
  179. var existUser entity.DtUser
  180. if err := db.Where("phone = ?", fullPhone).First(&existUser).Error; err == nil {
  181. ctx.Fail("phone_registered")
  182. return
  183. }
  184. // 查找邀请人
  185. var parentId int64 = 0
  186. if req.InviteCode != "" {
  187. var inviter entity.DtUser
  188. if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil {
  189. parentId = inviter.Id
  190. }
  191. }
  192. // 加密密码
  193. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
  194. if err != nil {
  195. ctx.Fail("register_failed")
  196. return
  197. }
  198. // 获取默认等级
  199. var defaultLevel entity.DtUserLevel
  200. db.Where("is_default = ?", 1).First(&defaultLevel)
  201. // 创建用户
  202. user := &entity.DtUser{
  203. Uid: generateUid(),
  204. Phone: fullPhone,
  205. Password: string(hashedPassword),
  206. Nickname: "用户" + req.Account[len(req.Account)-4:],
  207. ParentId: parentId,
  208. LevelId: defaultLevel.Id,
  209. InviteCode: generateInviteCode(),
  210. Status: 1,
  211. }
  212. tx := db.Begin()
  213. if err := tx.Create(user).Error; err != nil {
  214. tx.Rollback()
  215. ctx.Fail("register_failed")
  216. return
  217. }
  218. // 更新邀请人的直推人数和团队人数
  219. if parentId > 0 {
  220. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  221. Updates(map[string]interface{}{
  222. "direct_invite_count": db.Raw("direct_invite_count + 1"),
  223. "team_count": db.Raw("team_count + 1"),
  224. })
  225. // 更新上级的团队人数(多级)
  226. var parent entity.DtUser
  227. if err := tx.Where("id = ?", parentId).First(&parent).Error; err == nil && parent.ParentId > 0 {
  228. tx.Model(&entity.DtUser{}).Where("id = ?", parent.ParentId).
  229. Update("team_count", db.Raw("team_count + 1"))
  230. }
  231. }
  232. tx.Commit()
  233. // 删除验证码
  234. redisclient.DefaultClient().Del(c, codeKey)
  235. // 生成Token
  236. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  237. if err != nil {
  238. ctx.Fail("register_failed")
  239. return
  240. }
  241. ctx.OK(gin.H{
  242. "token": token,
  243. "user": gin.H{
  244. "id": user.Id,
  245. "uid": user.Uid,
  246. "nickname": user.Nickname,
  247. "avatar": user.Avatar,
  248. "phone": user.Phone,
  249. },
  250. })
  251. }
  252. // LoginByPassword 密码登录
  253. func (s *Server) LoginByPassword(c *gin.Context) {
  254. ctx := s.FromContext(c)
  255. db := s.DB()
  256. type Req struct {
  257. Type string `json:"type" binding:"required"` // phone/email
  258. Account string `json:"account" binding:"required"` // 手机号或邮箱
  259. Password string `json:"password" binding:"required"`
  260. AreaCode string `json:"areaCode"` // 国际区号
  261. }
  262. var req Req
  263. if err := c.ShouldBindJSON(&req); err != nil {
  264. ctx.Fail("invalid_params")
  265. return
  266. }
  267. // 完整手机号(带区号)
  268. fullPhone := req.Account
  269. if req.AreaCode != "" {
  270. fullPhone = req.AreaCode + req.Account
  271. }
  272. // 查找用户
  273. var user entity.DtUser
  274. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  275. ctx.Fail("user_not_found")
  276. return
  277. }
  278. // 检查用户状态
  279. if user.Status != 1 {
  280. ctx.Fail("user_disabled")
  281. return
  282. }
  283. // 验证密码
  284. if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
  285. ctx.Fail("invalid_password")
  286. return
  287. }
  288. // 更新登录时间
  289. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  290. Update("last_login_at", time.Now().Unix())
  291. // 生成Token
  292. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  293. if err != nil {
  294. ctx.Fail("login_failed")
  295. return
  296. }
  297. ctx.OK(gin.H{
  298. "token": token,
  299. "user": gin.H{
  300. "id": user.Id,
  301. "uid": user.Uid,
  302. "nickname": user.Nickname,
  303. "avatar": user.Avatar,
  304. "phone": user.Phone,
  305. },
  306. })
  307. }
  308. // LoginBySms 短信验证码登录
  309. func (s *Server) LoginBySms(c *gin.Context) {
  310. ctx := s.FromContext(c)
  311. db := s.DB()
  312. type Req struct {
  313. Phone string `json:"phone" binding:"required"`
  314. Code string `json:"code" binding:"required"`
  315. AreaCode string `json:"areaCode"` // 国际区号
  316. }
  317. var req Req
  318. if err := c.ShouldBindJSON(&req); err != nil {
  319. ctx.Fail("invalid_params")
  320. return
  321. }
  322. // 完整手机号(带区号)
  323. fullPhone := req.Phone
  324. if req.AreaCode != "" {
  325. fullPhone = req.AreaCode + req.Phone
  326. }
  327. // 验证短信验证码
  328. codeKey := fmt.Sprintf("smscode:login:%s", fullPhone)
  329. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  330. if err != nil || storedCode != req.Code {
  331. ctx.Fail("invalid_code")
  332. return
  333. }
  334. // 查找用户
  335. var user entity.DtUser
  336. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  337. ctx.Fail("user_not_found")
  338. return
  339. }
  340. // 检查用户状态
  341. if user.Status != 1 {
  342. ctx.Fail("user_disabled")
  343. return
  344. }
  345. // 更新登录时间
  346. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  347. Update("last_login_at", time.Now().Unix())
  348. // 删除验证码
  349. redisclient.DefaultClient().Del(c, codeKey)
  350. // 生成Token
  351. token, err2 := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  352. if err2 != nil {
  353. ctx.Fail("login_failed")
  354. return
  355. }
  356. ctx.OK(gin.H{
  357. "token": token,
  358. "user": gin.H{
  359. "id": user.Id,
  360. "uid": user.Uid,
  361. "nickname": user.Nickname,
  362. "avatar": user.Avatar,
  363. "phone": user.Phone,
  364. },
  365. })
  366. }
  367. // ResetPassword 重置密码
  368. func (s *Server) ResetPassword(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. Code string `json:"code" binding:"required"`
  375. NewPassword string `json:"newPassword" binding:"required,min=6"`
  376. AreaCode string `json:"areaCode"` // 国际区号
  377. }
  378. var req Req
  379. if err := c.ShouldBindJSON(&req); err != nil {
  380. ctx.Fail("invalid_params")
  381. return
  382. }
  383. // 完整手机号(带区号)
  384. fullPhone := req.Account
  385. if req.AreaCode != "" {
  386. fullPhone = req.AreaCode + req.Account
  387. }
  388. // 验证短信验证码
  389. codeKey := fmt.Sprintf("smscode:reset:%s", fullPhone)
  390. storedCode, err := redisclient.DefaultClient().Get(c, codeKey).Result()
  391. if err != nil || storedCode != req.Code {
  392. ctx.Fail("invalid_code")
  393. return
  394. }
  395. // 查找用户
  396. var user entity.DtUser
  397. if err := db.Where("phone = ?", fullPhone).First(&user).Error; err != nil {
  398. ctx.Fail("user_not_found")
  399. return
  400. }
  401. // 加密新密码
  402. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
  403. if err != nil {
  404. ctx.Fail("reset_failed")
  405. return
  406. }
  407. // 更新密码
  408. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  409. Update("password", string(hashedPassword))
  410. // 删除验证码
  411. redisclient.DefaultClient().Del(c, codeKey)
  412. ctx.OK(gin.H{
  413. "message": "password_reset_success",
  414. })
  415. }
  416. // OAuthLogin OAuth登录(Google/Zalo等)
  417. func (s *Server) OAuthLogin(c *gin.Context) {
  418. ctx := s.FromContext(c)
  419. db := s.DB()
  420. type Req struct {
  421. Provider string `json:"provider" binding:"required"` // google/zalo/telegram
  422. OpenId string `json:"openId" binding:"required"`
  423. Nickname string `json:"nickname"`
  424. Avatar string `json:"avatar"`
  425. InviteCode string `json:"inviteCode"`
  426. Extra string `json:"extra"` // Zalo: code_verifier
  427. }
  428. var req Req
  429. if err := c.ShouldBindJSON(&req); err != nil {
  430. ctx.Fail("invalid_params")
  431. return
  432. }
  433. // Zalo 特殊处理:需要用 code 换取 access_token
  434. if req.Provider == "zalo" && req.Extra != "" {
  435. zaloUser, err := s.getZaloUserInfo(req.OpenId, req.Extra)
  436. if err != nil {
  437. fmt.Printf("Zalo get user info error: %v\n", err)
  438. ctx.Fail("zalo_auth_failed")
  439. return
  440. }
  441. req.OpenId = zaloUser.ID
  442. req.Nickname = zaloUser.Name
  443. req.Avatar = zaloUser.Picture
  444. }
  445. // 查找是否已绑定
  446. var social entity.DtUserSocial
  447. if err := db.Where("platform = ? AND account = ?", req.Provider, req.OpenId).First(&social).Error; err == nil {
  448. // 已绑定,直接登录
  449. var user entity.DtUser
  450. if err := db.Where("id = ?", social.UserId).First(&user).Error; err != nil {
  451. ctx.Fail("user_not_found")
  452. return
  453. }
  454. if user.Status != 1 {
  455. ctx.Fail("user_disabled")
  456. return
  457. }
  458. // 更新登录时间
  459. db.Model(&entity.DtUser{}).Where("id = ?", user.Id).
  460. Update("last_login_at", time.Now().Unix())
  461. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  462. if err != nil {
  463. ctx.Fail("login_failed")
  464. return
  465. }
  466. ctx.OK(gin.H{
  467. "token": token,
  468. "user": gin.H{
  469. "id": user.Id,
  470. "uid": user.Uid,
  471. "nickname": user.Nickname,
  472. "avatar": user.Avatar,
  473. "phone": user.Phone,
  474. },
  475. })
  476. return
  477. }
  478. // 未绑定,创建新用户
  479. var parentId int64 = 0
  480. if req.InviteCode != "" {
  481. var inviter entity.DtUser
  482. if err := db.Where("invite_code = ?", req.InviteCode).First(&inviter).Error; err == nil {
  483. parentId = inviter.Id
  484. }
  485. }
  486. // 获取默认等级
  487. var defaultLevel entity.DtUserLevel
  488. db.Where("is_default = ?", 1).First(&defaultLevel)
  489. nickname := req.Nickname
  490. if nickname == "" {
  491. nickname = req.Provider + "用户"
  492. }
  493. // 为OAuth用户生成唯一占位手机号
  494. openIdPart := req.OpenId
  495. if len(openIdPart) > 16 {
  496. openIdPart = openIdPart[:16]
  497. }
  498. oauthPhone := fmt.Sprintf("oauth_%s_%s", req.Provider, openIdPart)
  499. user := &entity.DtUser{
  500. Uid: generateUid(),
  501. Phone: oauthPhone,
  502. Nickname: nickname,
  503. Avatar: req.Avatar,
  504. ParentId: parentId,
  505. LevelId: defaultLevel.Id,
  506. InviteCode: generateInviteCode(),
  507. Status: 1,
  508. }
  509. tx := db.Begin()
  510. if err := tx.Create(user).Error; err != nil {
  511. tx.Rollback()
  512. fmt.Printf("OAuth create user error: %v\n", err)
  513. ctx.Fail("login_failed")
  514. return
  515. }
  516. // 创建社交账号绑定
  517. social = entity.DtUserSocial{
  518. UserId: user.Id,
  519. Platform: req.Provider,
  520. Account: req.OpenId,
  521. Nickname: req.Nickname,
  522. Avatar: req.Avatar,
  523. Extra: "{}",
  524. }
  525. if err := tx.Create(&social).Error; err != nil {
  526. tx.Rollback()
  527. fmt.Printf("OAuth create social error: %v\n", err)
  528. ctx.Fail("login_failed")
  529. return
  530. }
  531. // 更新邀请人统计
  532. if parentId > 0 {
  533. tx.Model(&entity.DtUser{}).Where("id = ?", parentId).
  534. Updates(map[string]interface{}{
  535. "direct_invite_count": db.Raw("direct_invite_count + 1"),
  536. "team_count": db.Raw("team_count + 1"),
  537. })
  538. }
  539. tx.Commit()
  540. token, err := middleware.GenerateJWT(middleware.Member{ID: user.Id, Uid: user.Uid})
  541. if err != nil {
  542. fmt.Printf("OAuth generate JWT error: %v\n", err)
  543. ctx.Fail("login_failed")
  544. return
  545. }
  546. ctx.OK(gin.H{
  547. "token": token,
  548. "user": gin.H{
  549. "id": user.Id,
  550. "uid": user.Uid,
  551. "nickname": user.Nickname,
  552. "avatar": user.Avatar,
  553. "phone": user.Phone,
  554. },
  555. "isNew": true,
  556. })
  557. }
  558. // ZaloUser Zalo用户信息
  559. type ZaloUser struct {
  560. ID string `json:"id"`
  561. Name string `json:"name"`
  562. Picture string `json:"picture"`
  563. }
  564. // getZaloUserInfo 使用code获取Zalo用户信息
  565. func (s *Server) getZaloUserInfo(code, codeVerifier string) (*ZaloUser, error) {
  566. db := s.DB()
  567. // 获取 Zalo App ID 和 Secret
  568. var appIdConfig, secretConfig entity.DtConfig
  569. if err := db.Where("`key` = ?", entity.ConfigKeyZaloAppId).First(&appIdConfig).Error; err != nil {
  570. return nil, fmt.Errorf("zalo app_id not configured")
  571. }
  572. if err := db.Where("`key` = ?", entity.ConfigKeyZaloSecret).First(&secretConfig).Error; err != nil {
  573. return nil, fmt.Errorf("zalo secret not configured")
  574. }
  575. // 1. 用 code 换取 access_token
  576. tokenUrl := "https://oauth.zaloapp.com/v4/access_token"
  577. data := url.Values{}
  578. data.Set("app_id", appIdConfig.Value)
  579. data.Set("code", code)
  580. data.Set("code_verifier", codeVerifier)
  581. data.Set("grant_type", "authorization_code")
  582. req, _ := http.NewRequest("POST", tokenUrl, strings.NewReader(data.Encode()))
  583. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  584. req.Header.Set("secret_key", secretConfig.Value)
  585. client := &http.Client{Timeout: 10 * time.Second}
  586. resp, err := client.Do(req)
  587. if err != nil {
  588. return nil, fmt.Errorf("request token failed: %v", err)
  589. }
  590. defer resp.Body.Close()
  591. body, _ := io.ReadAll(resp.Body)
  592. fmt.Printf("Zalo token response: %s\n", string(body))
  593. var tokenResp struct {
  594. AccessToken string `json:"access_token"`
  595. ExpiresIn string `json:"expires_in"` // Zalo 返回的是字符串类型
  596. Error int `json:"error"`
  597. Message string `json:"message"`
  598. }
  599. if err := json.Unmarshal(body, &tokenResp); err != nil {
  600. return nil, fmt.Errorf("parse token response failed: %v", err)
  601. }
  602. if tokenResp.Error != 0 || tokenResp.AccessToken == "" {
  603. return nil, fmt.Errorf("get token failed: %s", tokenResp.Message)
  604. }
  605. // 2. 用 access_token 获取用户信息
  606. userUrl := "https://graph.zalo.me/v2.0/me?fields=id,name,picture"
  607. userReq, _ := http.NewRequest("GET", userUrl, nil)
  608. userReq.Header.Set("access_token", tokenResp.AccessToken)
  609. userResp, err := client.Do(userReq)
  610. if err != nil {
  611. return nil, fmt.Errorf("request user info failed: %v", err)
  612. }
  613. defer userResp.Body.Close()
  614. userBody, _ := io.ReadAll(userResp.Body)
  615. fmt.Printf("Zalo user response: %s\n", string(userBody))
  616. // Zalo API 错误响应格式: {"error": -501, "message": "..."}
  617. // Zalo API 成功响应格式: {"id": "...", "name": "...", "picture": {...}}
  618. var errorResp struct {
  619. Error int `json:"error"`
  620. Message string `json:"message"`
  621. }
  622. if err := json.Unmarshal(userBody, &errorResp); err == nil && errorResp.Error != 0 {
  623. return nil, fmt.Errorf("get user info failed: [%d] %s", errorResp.Error, errorResp.Message)
  624. }
  625. var userInfo struct {
  626. ID string `json:"id"`
  627. Name string `json:"name"`
  628. Picture struct {
  629. Data struct {
  630. URL string `json:"url"`
  631. } `json:"data"`
  632. } `json:"picture"`
  633. }
  634. if err := json.Unmarshal(userBody, &userInfo); err != nil {
  635. return nil, fmt.Errorf("parse user info failed: %v", err)
  636. }
  637. if userInfo.ID == "" {
  638. return nil, fmt.Errorf("get user info failed: empty user id")
  639. }
  640. return &ZaloUser{
  641. ID: userInfo.ID,
  642. Name: userInfo.Name,
  643. Picture: userInfo.Picture.Data.URL,
  644. }, nil
  645. }