Files
zt-closet-backend/pkg/wechat/mini.go
T
2026-09-11 01:41:52 +08:00

115 lines
3.4 KiB
Go

// 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-closet-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
}