- 新增用户与上传错误码文件及 Recovery 中间件 - 修复 router.go 导入与中间件链,注册用户路由并补 handler 错误码导入 - 补充用户状态常量与 UserGetByWechatOpenID 查询,修订刷新令牌 dto - 移除 upload 包 OTel 调用,新增 FileUploadCos 上传封装 - 实现 UserService 微信登录/刷新令牌/我的信息/改资料/上传头像 - 裁剪 main.go 配置段并移除 OTel 桥接,补启动与优雅退出 - 新增 configs 示例配置、global 配置结构及 go.mod 依赖
115 lines
3.4 KiB
Go
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-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
|
|
}
|