邹方成 45815bfb7d chore: 清理无用文件与优化代码结构
refactor(utils): 修复密码哈希比较逻辑错误
feat(user): 新增按状态筛选优惠券接口
docs: 添加虚拟发货与任务中心相关文档
fix(wechat): 修正Code2Session上下文传递问题
test: 补充订单折扣与积分转换测试用例
build: 更新配置文件与构建脚本
style: 清理多余的空行与注释
2025-12-18 17:35:55 +08:00

282 lines
7.4 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"
"context"
"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
}
// GetAccessTokenWithContext 获取微信 access_token使用 context.Context
// 用于后台任务等无 core.Context 的场景
func GetAccessTokenWithContext(ctx context.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 := "https://api.weixin.qq.com/cgi-bin/token"
client := httpclient.GetHttpClient()
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为空")
}
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
}