geoGo/pkg/wx.go

196 lines
5.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pkg
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"os"
"sync"
"time"
"net/http"
"errors"
"github.com/redis/go-redis/v9"
)
type WeChatLoginResponse struct {
OpenID string `json:"openid"` // 用户唯一标识
SessionKey string `json:"session_key"` // 会话密钥
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识(如果绑定了开放平台才有)
Errcode int `json:"errcode"` // 错误码0为成功
Errmsg string `json:"errmsg"` // 错误信息
}
func GetOpenID(appID, appSecret, code string) (openid string, err error) {
if os.Getenv("env") == "unit_test" {
return "test_123456", nil
}
// 1. 构建请求微信接口的 URL
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appID, appSecret, code)
// 2. 创建 HTTP 客户端(设置超时,避免阻塞)
client := &http.Client{
Timeout: 5 * time.Second,
// 在某些网络受限的环境(如本地测试跳过证书验证,生产环境建议去掉)
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // 生产环境建议设为 false
},
}
// 3. 发起 GET 请求
resp, err := client.Get(url)
if err != nil {
return "", fmt.Errorf("请求微信服务器失败: %w", err)
}
defer resp.Body.Close()
// 4. 读取返回的 Body
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取微信响应失败: %w", err)
}
// 5. 解析 JSON 数据
var wechatResp WeChatLoginResponse
err = json.Unmarshal(body, &wechatResp)
if err != nil {
return "", fmt.Errorf("解析微信响应 JSON 失败: %s, 原始数据: %s", err.Error(), string(body))
}
// 6. 检查微信接口返回的错误码
if wechatResp.Errcode != 0 {
// 这里可以根据不同的错误码做特殊处理,例如 code 无效、过期等
return "", fmt.Errorf("微信接口返回错误: code=%d, msg=%s", wechatResp.Errcode, wechatResp.Errmsg)
}
// 7. 检查 OpenID 是否为空(理论上不会,但防御性编程)
if wechatResp.OpenID == "" {
return "", errors.New("微信返回的 OpenID 为空")
}
// 8. 返回 OpenID
return wechatResp.OpenID, nil
}
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
}
var (
tokenMutex sync.Mutex
cacheKey = "wx:access_token"
)
// GetAccessToken 获取 access_token带本地缓存
func GetAccessToken(ctx context.Context, appID, appSecret string, rdb *redis.Client) (string, error) {
if rdb == nil {
return "", errors.New("缓存工具未提供")
}
cacheToken := rdb.Get(ctx, cacheKey).Val()
if cacheToken != "" {
return cacheToken, nil
}
tokenMutex.Lock()
defer tokenMutex.Unlock()
// 请求微信接口获取新的 access_token
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appID, appSecret)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
return "", fmt.Errorf("请求 access_token 失败: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var tokenRes AccessTokenResponse
if err := json.Unmarshal(body, &tokenRes); err != nil {
return "", fmt.Errorf("解析 access_token 响应失败: %w", err)
}
if tokenRes.Errcode != 0 {
return "", fmt.Errorf("获取 access_token 失败: code=%d, msg=%s", tokenRes.Errcode, tokenRes.Errmsg)
}
// 缓存 token提前5分钟过期避免边界情况
rdb.Set(ctx, cacheKey, tokenRes.AccessToken, time.Duration(tokenRes.ExpiresIn-300)*time.Second)
return tokenRes.AccessToken, nil
}
// PhoneInfo 定义手机号信息的结构体,与微信官方文档对齐 [citation:3][citation:8]
type PhoneInfo struct {
PhoneNumber string `json:"phoneNumber"` // 用户绑定的手机号(国外手机号会有区号)
PurePhoneNumber string `json:"purePhoneNumber"` // 没有区号的手机号
CountryCode string `json:"countryCode"` // 区号
Watermark struct {
Timestamp int64 `json:"timestamp"`
Appid string `json:"appid"`
} `json:"watermark"`
}
// PhoneInfoResponse 定义微信接口返回的完整结构
type PhoneInfoResponse struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
PhoneInfo PhoneInfo `json:"phone_info"`
}
// GetPhoneNumber 通过手机号 code 获取用户手机号
// 参数:
// - appID: 小程序的 AppID
// - appSecret: 小程序的 AppSecret
// - phoneCode: 前端通过 getPhoneNumber 获取的 code
//
// 返回:
// - *PhoneInfo: 手机号信息
// - error: 错误信息
func GetPhoneNumber(ctx context.Context, appID, appSecret, phoneCode string, rdb *redis.Client) (*PhoneInfo, error) {
// 1. 获取 access_token
accessToken, err := GetAccessToken(ctx, appID, appSecret, rdb)
if err != nil {
return nil, fmt.Errorf("获取 access_token 失败: %w", err)
}
// 2. 调用微信接口换取手机号 [citation:8]
url := fmt.Sprintf("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s", accessToken)
// 构建请求体
requestBody := map[string]string{
"code": phoneCode,
}
jsonBody, _ := json.Marshal(requestBody)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Post(url, "application/json", bytes.NewReader(jsonBody))
if err != nil {
return nil, fmt.Errorf("请求手机号接口失败: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var phoneResp PhoneInfoResponse
if err := json.Unmarshal(body, &phoneResp); err != nil {
return nil, fmt.Errorf("解析手机号响应失败: %s", string(body))
}
// 3. 检查微信接口返回的错误码 [citation:8]
if phoneResp.Errcode != 0 {
return nil, fmt.Errorf("微信接口返回错误: code=%d, msg=%s", phoneResp.Errcode, phoneResp.Errmsg)
}
return &phoneResp.PhoneInfo, nil
}