package wechat import ( "bytes" "encoding/json" "fmt" "net/http" "sync" "time" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/httpclient" ) // 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 }