feat: add clientapi: mp.oauth2

This commit is contained in:
lab 2021-10-28 00:59:04 +08:00
parent 0c70aab0a1
commit 408a941ccf
8 changed files with 469 additions and 0 deletions

1
clientapi/clientapi.go Normal file
View File

@ -0,0 +1 @@
package clientapi

View File

@ -0,0 +1,11 @@
package oauth2
import (
"git.esin.io/lab/weixin/clientapi/request"
)
type ClientCredential struct {
request.Error
AccessToken string `json:"access_token"`
ExpiresIn int32 `json:"expires_in"`
}

View File

@ -0,0 +1,137 @@
package oauth2
import (
"context"
"net/url"
"git.esin.io/lab/weixin/clientapi/request"
)
type Oauth2Client struct {
appId string
appSecret string
}
type GetCodeURLRequest struct {
RedirectURL, State, Scope string
}
func (c Oauth2Client) GetCodeURL(redirectURL, state, scope string) string {
endpoint := url.URL{
Scheme: "https",
Host: "open.weixin.qq.com",
Path: "/connect/oauth2/authorize",
RawQuery: url.Values{
"appid": {c.appId},
"redirect_uri": {redirectURL},
"response_type": {"code"},
"scope": {scope},
"state": {state},
}.Encode(),
Fragment: "wechat_redirect",
}
return endpoint.String()
}
func (c Oauth2Client) ExchangeToken(ctx context.Context, code string) (*Token, error) {
req := request.New(
"/sns/oauth2/access_token",
url.Values{
"grant_type": {"authorization_code"},
"appid": {c.appId},
"secret": {c.appSecret},
"code": {code},
})
var resp Token
if err := req.Get(ctx, &resp); err != nil {
return nil, err
}
if err := resp.Err(); err != nil {
return nil, err
}
return &resp, nil
}
func (c Oauth2Client) GetUserinfo(ctx context.Context, accessToken, openid string) (*Userinfo, error) {
req := request.New(
"/sns/userinfo",
url.Values{
"access_token": {accessToken},
"openid": {openid},
"lang": {"zh_CN"},
},
)
var resp Userinfo
if err := req.Get(ctx, &resp); err != nil {
return nil, err
}
if err := resp.Err(); err != nil {
return nil, err
}
return &resp, nil
}
func (c Oauth2Client) RefreshToken(ctx context.Context, refreshToken, openid string) (*Token, error) {
req := request.New(
"/sns/oauth2/refresh_token",
url.Values{
"grant_type": {"refresh_token"},
"appid": {c.appId},
"refresh_token": {refreshToken},
},
)
var resp Token
if err := req.Get(ctx, &resp); err != nil {
return nil, err
}
if err := resp.Err(); err != nil {
return nil, err
}
return &resp, nil
}
func (c Oauth2Client) ValidateToken(ctx context.Context, accessToken, openid string) error {
req := request.New(
"/sns/auth",
url.Values{
"access_token": {accessToken},
"openid": {openid},
},
)
var resp request.Error
if err := req.Get(ctx, &resp); err != nil {
return err
}
if err := resp.Err(); err != nil {
return err
}
return nil
}
func (c Oauth2Client) GetClientCredential(ctx context.Context) (*ClientCredential, error) {
req := request.New(
"/cgi-bin/token",
url.Values{
"grant_type": {"client_credential"},
"appid": {c.appId},
"secret": {c.appSecret},
},
)
var resp ClientCredential
if err := req.Get(ctx, &resp); err != nil {
return nil, err
}
if err := resp.Err(); err != nil {
return nil, err
}
return &resp, nil
}

View File

@ -0,0 +1,14 @@
package oauth2
import (
"git.esin.io/lab/weixin/clientapi/request"
)
type Token struct {
request.Error
AccessToken string `json:"access_token"` //获取到的凭证
ExpiresIn int32 `json:"expires_in"` //凭证有效时间,单位:秒
RefreshToken string `json:"refresh_token"` //有效期为30天当失效之后需要用户重新授
OpenID string `json:"openid"`
Scope string `json:"scope"`
}

View File

@ -0,0 +1,18 @@
package oauth2
import (
"git.esin.io/lab/weixin/clientapi/request"
)
type Userinfo struct {
request.Error
OpenID string `json:"openid"`
NickName string `json:"nickname"`
Sex int8 `json:"sex"`
Province string `json:"province"`
City string `json:"city"`
Country string `json:"country"`
HeadImgURL string `json:"headimgurl"`
Privilege []string `json:"privilege"`
UnionID string `json:"unionid"`
}

View File

@ -0,0 +1,168 @@
package request
var ErrCodeText = map[int32]string{
-1: "系统繁忙,此时请开发者稍候再试",
0: "请求成功",
40001: "获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口",
40002: "不合法的凭证类型",
40003: "不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID",
40004: "不合法的媒体文件类型",
40005: "不合法的文件类型",
40006: "不合法的文件大小",
40007: "不合法的媒体文件 id",
40008: "不合法的消息类型",
40009: "不合法的图片文件大小",
40010: "不合法的语音文件大小",
40011: "不合法的视频文件大小",
40012: "不合法的缩略图文件大小",
40013: "不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写",
40014: "不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口",
40015: "不合法的菜单类型",
40016: "不合法的按钮个数",
40017: "不合法的按钮类型",
40018: "不合法的按钮名字长度",
40019: "不合法的按钮 KEY 长度",
40020: "不合法的按钮 URL 长度",
40021: "不合法的菜单版本号",
40022: "不合法的子菜单级数",
40023: "不合法的子菜单按钮个数",
40024: "不合法的子菜单按钮类型",
40025: "不合法的子菜单按钮名字长度",
40026: "不合法的子菜单按钮 KEY 长度",
40027: "不合法的子菜单按钮 URL 长度",
40028: "不合法的自定义菜单使用用户",
40029: "无效的 oauth_code",
40030: "不合法的 refresh_token",
40031: "不合法的 openid 列表",
40032: "不合法的 openid 列表长度",
40033: "不合法的请求字符,不能包含 \\uxxxx 格式的字符",
40035: "不合法的参数",
40038: "不合法的请求格式",
40039: "不合法的 URL 长度",
40048: "无效的url",
40050: "不合法的分组 id",
40051: "分组名字不合法",
40060: "删除单篇图文时,指定的 article_idx 不合法",
40117: "分组名字不合法",
40118: "media_id 大小不合法",
40119: "button 类型错误",
40120: "子 button 类型错误",
40121: "不合法的 media_id 类型",
40125: "无效的appsecret",
40132: "微信号不合法",
40137: "不支持的图片格式",
40155: "请勿添加其他公众号的主页链接",
40163: "oauth_code已使用",
41001: "缺少 access_token 参数",
41002: "缺少 appid 参数",
41003: "缺少 refresh_token 参数",
41004: "缺少 secret 参数",
41005: "缺少多媒体文件数据",
41006: "缺少 media_id 参数",
41007: "缺少子菜单数据",
41008: "缺少 oauth code",
41009: "缺少 openid",
42001: "access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明",
42002: "refresh_token 超时",
42003: "oauth_code 超时",
42007: "用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权",
43001: "需要 GET 请求",
43002: "需要 POST 请求",
43003: "需要 HTTPS 请求",
43004: "需要接收者关注",
43005: "需要好友关系",
43019: "需要将接收者从黑名单中移除",
44001: "多媒体文件为空",
44002: "POST 的数据包为空",
44003: "图文消息内容为空",
44004: "文本消息内容为空",
45001: "多媒体文件大小超过限制",
45002: "消息内容超过限制",
45003: "标题字段超过限制",
45004: "描述字段超过限制",
45005: "链接字段超过限制",
45006: "图片链接字段超过限制",
45007: "语音播放时间超过限制",
45008: "图文消息超过限制",
45009: "接口调用超过限制",
45010: "创建菜单个数超过限制",
45011: "API 调用太频繁,请稍候再试",
45015: "回复时间超过限制",
45016: "系统分组,不允许修改",
45017: "分组名字过长",
45018: "分组数量超过上限",
45047: "客服接口下行条数超过上限",
45064: "创建菜单包含未关联的小程序",
45065: "相同 clientmsgid 已存在群发记录,返回数据中带有已存在的群发任务的 msgid",
45066: "相同 clientmsgid 重试速度过快请间隔1分钟重试",
45067: "clientmsgid 长度超过限制",
46001: "不存在媒体数据",
46002: "不存在的菜单版本",
46003: "不存在的菜单数据",
46004: "不存在的用户",
47001: "解析 JSON/XML 内容错误",
48001: "api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限",
48002: "粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” ",
48004: "api 接口被封禁,请登录 mp.weixin.qq.com 查看详情",
48005: "api 禁止删除被自动回复和自定义菜单引用的素材",
48006: "api 禁止清零调用次数,因为清零次数达到上限",
48008: "没有该类型消息的发送权限",
50001: "用户未授权该 api",
50002: "用户受限,可能是违规后接口被封禁",
50005: "用户未关注公众号",
61451: "参数错误 (invalid parameter)",
61452: "无效客服账号 (invalid kf_account)",
61453: "客服帐号已存在 (kf_account exsited)",
61454: "客服帐号名长度超过限制 ( 仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号 )(invalid   kf_acount length)",
61455: "客服帐号名包含非法字符 ( 仅允许英文 + 数字 )(illegal character in     kf_account)",
61456: "客服帐号个数超过限制 (10 个客服账号 )(kf_account count exceeded)",
61457: "无效头像文件类型 (invalid   file type)",
61450: "系统错误 (system error)",
61500: "日期格式错误",
63001: "部分参数为空",
63002: "无效的签名",
65301: "不存在此 menuid 对应的个性化菜单",
65302: "没有相应的用户",
65303: "没有默认菜单,不能创建个性化菜单",
65304: "MatchRule 信息为空",
65305: "个性化菜单数量受限",
65306: "不支持个性化菜单的帐号",
65307: "个性化菜单信息为空",
65308: "包含没有响应类型的 button",
65309: "个性化菜单开关处于关闭状态",
65310: "填写了省份或城市信息,国家信息不能为空",
65311: "填写了城市信息,省份信息不能为空",
65312: "不合法的国家信息",
65313: "不合法的省份信息",
65314: "不合法的城市信息",
65316: "该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接)",
65317: "不合法的 URL",
87009: "无效的签名",
9001001: "POST 数据参数不合法",
9001002: "远端服务不可用",
9001003: "Ticket 不合法",
9001004: "获取摇周边用户信息失败",
9001005: "获取商户信息失败",
9001006: "获取 OpenID 失败",
9001007: "上传文件缺失",
9001008: "上传素材的文件类型不合法",
9001009: "上传素材的文件尺寸不合法",
9001010: "上传失败",
9001020: "帐号不合法",
9001021: "已有设备激活率低于 50% ,不能新增设备",
9001022: "设备申请数不合法,必须为大于 0 的数字",
9001023: "已存在审核中的设备 ID 申请",
9001024: "一次查询设备 ID 数量不能超过 50",
9001025: "设备 ID 不合法",
9001026: "页面 ID 不合法",
9001027: "页面参数不合法",
9001028: "一次删除页面 ID 数量不能超过 10",
9001029: "页面已应用在设备中,请先解除应用关系再删除",
9001030: "一次查询页面 ID 数量不能超过 50",
9001031: "时间区间不合法",
9001032: "保存设备与页面的绑定关系参数错误",
9001033: "门店 ID 不合法",
9001034: "设备备注信息过长",
9001035: "设备申请参数不合法",
9001036: "查询起始值 begin 不合法",
}

View File

@ -0,0 +1,25 @@
package request
import "fmt"
type Error struct {
Code int32 `json:"errcode"`
Msg string `json:"errmsg"`
}
func (err Error) IsZero() bool {
// return reflect.DeepEqual(err, Error{}) || reflect.DeepEqual(err, Error{Code: 0, Msg: "ok"})
return err.Code == 0
}
func (err Error) Error() string {
text := ErrCodeText[err.Code]
return fmt.Sprintf("%d:%s, %s", err.Code, err.Msg, text)
}
func (err Error) Err() error {
if err.Code == 0 {
return nil
}
return err
}

View File

@ -0,0 +1,95 @@
package request
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
)
const (
defaultHost = "api.weixin.qq.com"
)
type Option func(*Request)
type Request struct {
Host string
Path string
Params url.Values
Fragment string
}
func WithHost(host string) Option {
return func(r *Request) {
r.Host = host
}
}
func WithFragment(fragment string) Option {
return func(r *Request) {
r.Fragment = fragment
}
}
func New(path string, params url.Values, opts ...Option) *Request {
r := Request{
Host: defaultHost,
Path: path,
Params: params,
}
for _, opt := range opts {
opt(&r)
}
return &r
}
func (r Request) Endpoint() string {
u := url.URL{
Scheme: "https",
Host: r.Host,
Path: r.Path,
RawQuery: r.Params.Encode(),
Fragment: r.Fragment,
}
return u.String()
}
func (r Request) Get(ctx context.Context, result interface{}) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.Endpoint(), nil)
if err != nil {
return nil
}
return r.SendRequest(req, result)
}
func (r Request) Post(ctx context.Context, data interface{}, result interface{}) error {
dataBytes, err := json.Marshal(data)
if err != nil {
return err
}
body := bytes.NewReader(dataBytes)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.Endpoint(), body)
if err != nil {
return nil
}
return r.SendRequest(req, result)
}
func (r Request) SendRequest(req *http.Request, result interface{}) error {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(body, &result)
}