262 lines
6.5 KiB
Go
262 lines
6.5 KiB
Go
package wechat
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"mini-chat/internal/pkg/core"
|
||
"mini-chat/internal/pkg/httpclient"
|
||
"net/http"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// AccessTokenRequest 获取 access_token 请求参数
|
||
type AccessTokenRequest struct {
|
||
AppID string `json:"appid"`
|
||
AppSecret string `json:"secret"`
|
||
GrantType string `json:"grant_type"`
|
||
}
|
||
|
||
// AccessTokenResponse 获取 access_token 响应
|
||
type AccessTokenResponse struct {
|
||
AccessToken string `json:"access_token"`
|
||
ExpiresIn int `json:"expires_in"`
|
||
ErrCode int `json:"errcode,omitempty"`
|
||
ErrMsg string `json:"errmsg,omitempty"`
|
||
}
|
||
|
||
// TokenCache access_token 缓存结构
|
||
type TokenCache struct {
|
||
Token string
|
||
ExpiresAt time.Time
|
||
mutex sync.RWMutex
|
||
}
|
||
|
||
// 全局 token 缓存
|
||
var globalTokenCache = &TokenCache{}
|
||
|
||
// WechatConfig 微信配置
|
||
type WechatConfig struct {
|
||
AppID string
|
||
AppSecret string
|
||
}
|
||
|
||
// QRCodeRequest 获取小程序码请求参数
|
||
type QRCodeRequest struct {
|
||
Path string `json:"path"`
|
||
Width int `json:"width,omitempty"`
|
||
AutoColor bool `json:"auto_color,omitempty"`
|
||
LineColor *LineColor `json:"line_color,omitempty"`
|
||
IsHyaline bool `json:"is_hyaline,omitempty"`
|
||
EnvVersion string `json:"release,omitempty"`
|
||
}
|
||
|
||
type LineColor struct {
|
||
R int `json:"r"`
|
||
G int `json:"g"`
|
||
B int `json:"b"`
|
||
}
|
||
|
||
// QRCodeResponse 获取小程序码响应
|
||
type QRCodeResponse struct {
|
||
Buffer []byte `json:"-"`
|
||
ErrCode int `json:"errcode,omitempty"`
|
||
ErrMsg string `json:"errmsg,omitempty"`
|
||
}
|
||
|
||
// QRCodeError 微信API错误响应
|
||
type QRCodeError struct {
|
||
ErrCode int `json:"errcode"`
|
||
ErrMsg string `json:"errmsg"`
|
||
}
|
||
|
||
func (e *QRCodeError) Error() string {
|
||
return fmt.Sprintf("微信API错误: errcode=%d, errmsg=%s", e.ErrCode, e.ErrMsg)
|
||
}
|
||
|
||
// GetAccessToken 获取微信 access_token
|
||
func GetAccessToken(ctx core.StdContext, config *WechatConfig) (string, error) {
|
||
if config == nil {
|
||
return "", fmt.Errorf("微信配置不能为空")
|
||
}
|
||
|
||
if config.AppID == "" {
|
||
return "", fmt.Errorf("AppID 不能为空")
|
||
}
|
||
|
||
if config.AppSecret == "" {
|
||
return "", fmt.Errorf("AppSecret 不能为空")
|
||
}
|
||
|
||
// 检查缓存
|
||
globalTokenCache.mutex.RLock()
|
||
if globalTokenCache.Token != "" && time.Now().Before(globalTokenCache.ExpiresAt) {
|
||
token := globalTokenCache.Token
|
||
globalTokenCache.mutex.RUnlock()
|
||
return token, nil
|
||
}
|
||
globalTokenCache.mutex.RUnlock()
|
||
|
||
// 缓存过期或不存在,重新获取
|
||
globalTokenCache.mutex.Lock()
|
||
defer globalTokenCache.mutex.Unlock()
|
||
|
||
// 双重检查,防止并发情况下重复请求
|
||
if globalTokenCache.Token != "" && time.Now().Before(globalTokenCache.ExpiresAt) {
|
||
return globalTokenCache.Token, nil
|
||
}
|
||
|
||
// 构建请求URL
|
||
url := "https://api.weixin.qq.com/cgi-bin/token"
|
||
|
||
// 发送HTTP请求
|
||
client := httpclient.GetHttpClientWithContext(ctx)
|
||
resp, err := client.R().
|
||
SetQueryParams(map[string]string{
|
||
"grant_type": "client_credential",
|
||
"appid": config.AppID,
|
||
"secret": config.AppSecret,
|
||
}).
|
||
Get(url)
|
||
|
||
if err != nil {
|
||
return "", fmt.Errorf("获取access_token失败: %v", err)
|
||
}
|
||
|
||
if resp.StatusCode() != http.StatusOK {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode())
|
||
}
|
||
|
||
var tokenResp AccessTokenResponse
|
||
if err := json.Unmarshal(resp.Body(), &tokenResp); err != nil {
|
||
return "", fmt.Errorf("解析access_token响应失败: %v", err)
|
||
}
|
||
|
||
if tokenResp.ErrCode != 0 {
|
||
return "", fmt.Errorf("获取access_token失败: errcode=%d, errmsg=%s", tokenResp.ErrCode, tokenResp.ErrMsg)
|
||
}
|
||
|
||
if tokenResp.AccessToken == "" {
|
||
return "", fmt.Errorf("获取到的access_token为空")
|
||
}
|
||
|
||
expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-300) * time.Second)
|
||
globalTokenCache.Token = tokenResp.AccessToken
|
||
globalTokenCache.ExpiresAt = expiresAt
|
||
|
||
return tokenResp.AccessToken, nil
|
||
}
|
||
|
||
// GetQRCodeWithConfig 通过微信配置获取小程序码(自动获取access_token)
|
||
// config: 微信配置(AppID 和 AppSecret)
|
||
// request: 请求参数
|
||
// 返回: 成功时返回图片二进制数据,失败时返回错误
|
||
func GetQRCodeWithConfig(ctx core.StdContext, config *WechatConfig, request *QRCodeRequest) (*QRCodeResponse, error) {
|
||
// 自动获取 access_token
|
||
accessToken, err := GetAccessToken(ctx, config)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取access_token失败: %v", err)
|
||
}
|
||
|
||
return GetQRCode(ctx, accessToken, request)
|
||
}
|
||
|
||
// GetQRCode 获取小程序码
|
||
// accessToken: 接口调用凭证
|
||
// request: 请求参数
|
||
// 返回: 成功时返回图片二进制数据,失败时返回错误
|
||
func GetQRCode(ctx core.StdContext, accessToken string, request *QRCodeRequest) (*QRCodeResponse, error) {
|
||
if accessToken == "" {
|
||
return nil, fmt.Errorf("access_token 不能为空")
|
||
}
|
||
|
||
if request == nil {
|
||
return nil, fmt.Errorf("请求参数不能为空")
|
||
}
|
||
|
||
if request.Path == "" {
|
||
return nil, fmt.Errorf("path 参数不能为空")
|
||
}
|
||
|
||
if len(request.Path) > 1024 {
|
||
return nil, fmt.Errorf("path 参数长度不能超过1024个字符")
|
||
}
|
||
|
||
// 设置默认值
|
||
if request.Width == 0 {
|
||
request.Width = 430
|
||
}
|
||
if request.Width < 280 {
|
||
request.Width = 280
|
||
}
|
||
if request.Width > 1280 {
|
||
request.Width = 1280
|
||
}
|
||
|
||
url := fmt.Sprintf("https://api.weixin.qq.com/wxa/getwxacode?access_token=%s", accessToken)
|
||
|
||
requestBody, err := json.Marshal(request)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("序列化请求参数失败: %v", err)
|
||
}
|
||
|
||
client := httpclient.GetHttpClientWithContext(ctx)
|
||
resp, err := client.R().
|
||
SetHeader("Content-Type", "application/json").
|
||
SetBody(requestBody).
|
||
Post(url)
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
|
||
}
|
||
|
||
if resp.StatusCode() != http.StatusOK {
|
||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode())
|
||
}
|
||
|
||
responseBody := resp.Body()
|
||
|
||
if isJSONResponse(responseBody) {
|
||
var qrError QRCodeError
|
||
if err := json.Unmarshal(responseBody, &qrError); err != nil {
|
||
return nil, fmt.Errorf("解析错误响应失败: %v", err)
|
||
}
|
||
return nil, &qrError
|
||
}
|
||
|
||
return &QRCodeResponse{
|
||
Buffer: responseBody,
|
||
}, nil
|
||
}
|
||
|
||
func isJSONResponse(data []byte) bool {
|
||
if len(data) == 0 {
|
||
return false
|
||
}
|
||
|
||
trimmed := bytes.TrimSpace(data)
|
||
return len(trimmed) > 0 && trimmed[0] == '{'
|
||
}
|
||
|
||
// GenerateQRCode 最简化的小程序码生成接口
|
||
func GenerateQRCode(appID, appSecret, path string) ([]byte, error) {
|
||
ctx := core.StdContext{}
|
||
|
||
config := &WechatConfig{
|
||
AppID: appID,
|
||
AppSecret: appSecret,
|
||
}
|
||
|
||
request := &QRCodeRequest{
|
||
Path: path,
|
||
}
|
||
|
||
response, err := GetQRCodeWithConfig(ctx, config, request)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return response.Buffer, nil
|
||
}
|