邹方成 e4d4258918 refactor(wechat): 重构微信二维码生成接口上下文处理
移除全局token缓存逻辑,统一使用core.Context接口
修改GenerateQRCode方法签名,增加上下文参数
更新相关调用链以适配新的上下文处理方式
2025-10-19 00:23:44 +08:00

240 lines
6.0 KiB
Go
Raw 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 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.Context, config *WechatConfig) (string, error) {
if config == nil {
return "", fmt.Errorf("微信配置不能为空")
}
if config.AppID == "" {
return "", fmt.Errorf("AppID 不能为空")
}
if config.AppSecret == "" {
return "", fmt.Errorf("AppSecret 不能为空")
}
// 构建请求URL
url := "https://api.weixin.qq.com/cgi-bin/token"
// 发送HTTP请求
client := httpclient.GetHttpClientWithContext(ctx.RequestContext())
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.Context, 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.Context, 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.RequestContext())
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(ctx core.Context, appID, appSecret, path string) ([]byte, error) {
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
}