// Package wechat 封装微信小程序登录与微信支付 V3 的真实对接能力,仅依赖 Go 标准库。 // // 安全说明: // - AppSecret / APIv3Key / 商户私钥均来自配置(禁止硬编码)。 // - 仅访问微信固定域名(api.weixin.qq.com / api.mch.weixin.qq.com),无 SSRF 风险。 package wechat import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "git.lzcwy.cn/zhengchiliu/zt-online-game-backend/global" ) // code2SessionURL 微信小程序登录凭证校验接口。 const code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session" // code2SessionResp 微信 code2session 响应。 type code2SessionResp struct { OpenID string `json:"openid"` SessionKey string `json:"session_key"` UnionID string `json:"unionid"` ErrCode int `json:"errcode"` ErrMsg string `json:"errmsg"` } // Code2SessionResult code2session 结果。 type Code2SessionResult struct { OpenID string SessionKey string UnionID string } // miniHTTPClient 小程序接口 HTTP 客户端(较短超时,微信接口通常很快)。 var miniHTTPClient = &http.Client{Timeout: 10 * time.Second} // Code2Session 使用小程序登录 code 换取 openid / session_key。 // // 依赖配置 global.WechatMiniSetting.AppID / AppSecret。 // 常见错误:40029 code 无效、45011 频率限制、40163 code 已使用。 func Code2Session(ctx context.Context, code string) (*Code2SessionResult, error) { if global.WechatMiniSetting == nil || global.WechatMiniSetting.AppID == "" || global.WechatMiniSetting.AppSecret == "" { return nil, fmt.Errorf("wechat mini not configured") } q := url.Values{} q.Set("appid", global.WechatMiniSetting.AppID) q.Set("secret", global.WechatMiniSetting.AppSecret) q.Set("js_code", code) q.Set("grant_type", "authorization_code") reqURL := code2SessionURL + "?" + q.Encode() request, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, fmt.Errorf("code2session build request: %w", err) } response, err := miniHTTPClient.Do(request) if err != nil { return nil, fmt.Errorf("code2session do request: %w", err) } defer response.Body.Close() data, err := io.ReadAll(response.Body) if err != nil { return nil, fmt.Errorf("code2session read response: %w", err) } if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("code2session http status %d: %s", response.StatusCode, string(data)) } var r code2SessionResp if err := json.Unmarshal(data, &r); err != nil { return nil, fmt.Errorf("code2session decode: %w", err) } if r.ErrCode != 0 { return nil, &Code2SessionError{Code: r.ErrCode, Msg: r.ErrMsg} } if r.OpenID == "" { return nil, fmt.Errorf("code2session empty openid") } return &Code2SessionResult{ OpenID: r.OpenID, SessionKey: r.SessionKey, UnionID: r.UnionID, }, nil } // Code2SessionError 微信 code2session 业务错误,携带 errcode 便于上层区分 code 无效等场景。 type Code2SessionError struct { Code int Msg string } func (e *Code2SessionError) Error() string { return fmt.Sprintf("code2session errcode=%d, errmsg=%s", e.Code, e.Msg) } // IsInvalidCode 判断是否为「code 无效 / 已使用 / 过期」类错误。 func (e *Code2SessionError) IsInvalidCode() bool { switch e.Code { case 40029, // js_code 无效 40163, // code been used 41008: // missing code return true } return false }