client.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. package wechatmini
  2. import (
  3. "context"
  4. "crypto/sha1"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "time"
  13. )
  14. type Client struct {
  15. appID string
  16. appSecret string
  17. devPrefix string
  18. httpClient *http.Client
  19. }
  20. type Session struct {
  21. AppID string
  22. OpenID string
  23. UnionID string
  24. SessionKey string
  25. }
  26. type code2SessionResponse struct {
  27. OpenID string `json:"openid"`
  28. SessionKey string `json:"session_key"`
  29. UnionID string `json:"unionid"`
  30. ErrCode int `json:"errcode"`
  31. ErrMsg string `json:"errmsg"`
  32. }
  33. func NewClient(appID, appSecret, devPrefix string) *Client {
  34. return &Client{
  35. appID: appID,
  36. appSecret: appSecret,
  37. devPrefix: devPrefix,
  38. httpClient: &http.Client{Timeout: 8 * time.Second},
  39. }
  40. }
  41. func (c *Client) ExchangeCode(ctx context.Context, code string) (*Session, error) {
  42. code = strings.TrimSpace(code)
  43. if code == "" {
  44. return nil, fmt.Errorf("wechat code is required")
  45. }
  46. if c.devPrefix != "" && strings.HasPrefix(code, c.devPrefix) {
  47. suffix := strings.TrimPrefix(code, c.devPrefix)
  48. if suffix == "" {
  49. suffix = "default"
  50. }
  51. return &Session{
  52. AppID: fallbackString(c.appID, "dev-mini-app"),
  53. OpenID: "dev_openid_" + normalizeDevID(suffix),
  54. UnionID: "",
  55. }, nil
  56. }
  57. if c.appID == "" || c.appSecret == "" {
  58. return nil, fmt.Errorf("wechat mini app credentials are not configured")
  59. }
  60. values := url.Values{}
  61. values.Set("appid", c.appID)
  62. values.Set("secret", c.appSecret)
  63. values.Set("js_code", code)
  64. values.Set("grant_type", "authorization_code")
  65. req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.weixin.qq.com/sns/jscode2session?"+values.Encode(), nil)
  66. if err != nil {
  67. return nil, err
  68. }
  69. resp, err := c.httpClient.Do(req)
  70. if err != nil {
  71. return nil, err
  72. }
  73. defer resp.Body.Close()
  74. body, err := io.ReadAll(resp.Body)
  75. if err != nil {
  76. return nil, err
  77. }
  78. var parsed code2SessionResponse
  79. if err := json.Unmarshal(body, &parsed); err != nil {
  80. return nil, err
  81. }
  82. if parsed.ErrCode != 0 {
  83. return nil, fmt.Errorf("wechat code2session failed: %d %s", parsed.ErrCode, parsed.ErrMsg)
  84. }
  85. if parsed.OpenID == "" {
  86. return nil, fmt.Errorf("wechat code2session returned empty openid")
  87. }
  88. return &Session{
  89. AppID: c.appID,
  90. OpenID: parsed.OpenID,
  91. UnionID: parsed.UnionID,
  92. SessionKey: parsed.SessionKey,
  93. }, nil
  94. }
  95. func normalizeDevID(value string) string {
  96. sum := sha1.Sum([]byte(value))
  97. return hex.EncodeToString(sum[:])[:16]
  98. }
  99. func fallbackString(value, fallback string) string {
  100. if value != "" {
  101. return value
  102. }
  103. return fallback
  104. }